tlong-ds commited on
Commit
d810052
·
1 Parent(s): 67c6671
Files changed (36) hide show
  1. .dockerignore +26 -0
  2. Dockerfile +31 -0
  3. requirements.txt +29 -0
  4. services/.DS_Store +0 -0
  5. services/__pycache__/__init__.cpython-312.pyc +0 -0
  6. services/api/.DS_Store +0 -0
  7. services/api/__pycache__/__init__.cpython-312.pyc +0 -0
  8. services/api/__pycache__/api_endpoints.cpython-312.pyc +0 -0
  9. services/api/__pycache__/chat_endpoints.cpython-312.pyc +0 -0
  10. services/api/__pycache__/video_upload.cpython-312.pyc +0 -0
  11. services/api/api_endpoints.py +1853 -0
  12. services/api/chat_endpoints.py +102 -0
  13. services/api/chatbot/__pycache__/config.cpython-312.pyc +0 -0
  14. services/api/chatbot/__pycache__/config.cpython-313.pyc +0 -0
  15. services/api/chatbot/__pycache__/core.cpython-312.pyc +0 -0
  16. services/api/chatbot/__pycache__/core.cpython-313.pyc +0 -0
  17. services/api/chatbot/__pycache__/llm.cpython-312.pyc +0 -0
  18. services/api/chatbot/__pycache__/llm.cpython-313.pyc +0 -0
  19. services/api/chatbot/__pycache__/prompts.cpython-312.pyc +0 -0
  20. services/api/chatbot/__pycache__/prompts.cpython-313.pyc +0 -0
  21. services/api/chatbot/__pycache__/retrieval.cpython-312.pyc +0 -0
  22. services/api/chatbot/__pycache__/retrieval.cpython-313.pyc +0 -0
  23. services/api/chatbot/config.py +18 -0
  24. services/api/chatbot/core.py +117 -0
  25. services/api/chatbot/llm.py +29 -0
  26. services/api/chatbot/memory.py +0 -0
  27. services/api/chatbot/prompts.py +73 -0
  28. services/api/chatbot/retrieval.py +531 -0
  29. services/api/chatbot/self_query.py +0 -0
  30. services/api/db/__pycache__/__init__.cpython-312.pyc +0 -0
  31. services/api/db/__pycache__/auth.cpython-312.pyc +0 -0
  32. services/api/db/__pycache__/db_connection.cpython-312.pyc +0 -0
  33. services/api/db/__pycache__/token_utils.cpython-312.pyc +0 -0
  34. services/api/db/auth.py +437 -0
  35. services/api/db/token_utils.py +50 -0
  36. services/api/skills.csv +21 -0
.dockerignore ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Version control
2
+ .git
3
+ .gitignore
4
+
5
+ # Python
6
+ __pycache__
7
+ *.pyc
8
+ *.pyo
9
+ *.pyd
10
+ .Python
11
+ venv
12
+ .env
13
+
14
+ # IDE
15
+ .vscode
16
+ .idea
17
+
18
+ # React frontend
19
+ react-frontend/node_modules
20
+ react-frontend/build
21
+ react-frontend/.env*
22
+
23
+ # Misc
24
+ .DS_Store
25
+ README.md
26
+ *.log
Dockerfile ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.9 slim image as base
2
+ FROM python:3.9-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Set environment variables
8
+ ENV PYTHONUNBUFFERED=1 \
9
+ PYTHONDONTWRITEBYTECODE=1
10
+
11
+ # Install system dependencies
12
+ RUN apt-get update && \
13
+ apt-get install -y --no-install-recommends \
14
+ build-essential \
15
+ curl \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ # Copy requirements file
19
+ COPY requirements.txt .
20
+
21
+ # Install Python dependencies
22
+ RUN pip install --no-cache-dir -r requirements.txt
23
+
24
+ # Copy the rest of the application
25
+ COPY . .
26
+
27
+ # Expose the port
28
+ EXPOSE 7860
29
+
30
+ # Command to run the application
31
+ CMD ["uvicorn", "services.api.db.auth:app", "--host", "0.0.0.0", "--port", "7860"]
requirements.txt ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core dependencies
2
+ fastapi==0.109.2
3
+ uvicorn==0.27.1
4
+ python-multipart==0.0.7
5
+ python-dotenv==1.0.1
6
+
7
+ # Database
8
+ pymysql==1.1.0
9
+ bcrypt==4.1.2
10
+ SQLAlchemy==2.0.27
11
+
12
+ # Cloud Services
13
+ boto3==1.34.34
14
+ awscli==1.32.34
15
+
16
+ # Auth
17
+ python-jose==3.3.0
18
+ pyasn1==0.4.8
19
+ pyasn1-modules==0.2.8
20
+
21
+ # AI/ML
22
+ google-generativeai==0.3.2
23
+ langchain==0.1.4
24
+ langchain-community==0.0.16
25
+ qdrant-client==1.7.0
26
+ sentence-transformers==2.5.1
27
+
28
+ # Data Processing
29
+ pandas==2.2.0
services/.DS_Store ADDED
Binary file (6.15 kB). View file
 
services/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (224 Bytes). View file
 
services/api/.DS_Store ADDED
Binary file (6.15 kB). View file
 
services/api/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (228 Bytes). View file
 
services/api/__pycache__/api_endpoints.cpython-312.pyc ADDED
Binary file (73.7 kB). View file
 
services/api/__pycache__/chat_endpoints.cpython-312.pyc ADDED
Binary file (4.99 kB). View file
 
services/api/__pycache__/video_upload.cpython-312.pyc ADDED
Binary file (4.11 kB). View file
 
services/api/api_endpoints.py ADDED
@@ -0,0 +1,1853 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, Request, Cookie, UploadFile, File, Form
2
+ from typing import List, Optional, Dict
3
+ from pydantic import BaseModel
4
+ from services.api.db.token_utils import decode_token
5
+ from dotenv import load_dotenv
6
+ import os
7
+ import pymysql
8
+ import pandas as pd
9
+ import boto3
10
+ import json
11
+ from botocore.exceptions import ClientError
12
+ from datetime import datetime
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+
15
+ # Load environment variables
16
+ load_dotenv()
17
+ MYSQL_USER = os.getenv("MYSQL_USER")
18
+ MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD")
19
+ MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost")
20
+ MYSQL_DB = os.getenv("MYSQL_DB")
21
+ MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
22
+
23
+ # AWS Configuration
24
+ AWS_ACCESS_KEY = os.getenv("AWS_ACCESS_KEY_ID")
25
+ AWS_SECRET_KEY = os.getenv("AWS_SECRET_ACCESS_KEY")
26
+ REGION = os.getenv("REGION_NAME", "ap-southeast-1")
27
+
28
+ # Initialize S3 client
29
+ s3 = boto3.client('s3',
30
+ aws_access_key_id=AWS_ACCESS_KEY,
31
+ aws_secret_access_key=AWS_SECRET_KEY,
32
+ region_name=REGION
33
+ )
34
+
35
+ router = APIRouter(
36
+ prefix="/api",
37
+ tags=["courses"],
38
+ responses={404: {"description": "Not found"}},
39
+ )
40
+
41
+ # Simple test endpoint to verify routing
42
+ @router.post("/test")
43
+ async def test_post():
44
+ return {"message": "POST test endpoint works"}
45
+
46
+ def connect_db():
47
+ try:
48
+ print(f"Connecting to database {MYSQL_DB} on {MYSQL_HOST}:{MYSQL_PORT}")
49
+ connection = pymysql.connect(
50
+ host=MYSQL_HOST,
51
+ user=MYSQL_USER,
52
+ password=MYSQL_PASSWORD,
53
+ database=MYSQL_DB,
54
+ port=MYSQL_PORT
55
+ )
56
+ print("Database connection successful")
57
+ return connection
58
+ except Exception as e:
59
+ print(f"Database connection error: {str(e)}")
60
+ raise HTTPException(status_code=500, detail=f"Database connection error: {str(e)}")
61
+
62
+ # Models
63
+ class Course(BaseModel):
64
+ id: int
65
+ name: str
66
+ instructor: str
67
+ description: Optional[str]
68
+ rating: Optional[float] = None
69
+ enrolled: Optional[int] = 0
70
+
71
+ class QuestionOption(BaseModel):
72
+ text: str
73
+ isCorrect: bool = False
74
+
75
+ class QuizQuestion(BaseModel):
76
+ question: str
77
+ options: list[str]
78
+ correctAnswer: int
79
+
80
+ class Quiz(BaseModel):
81
+ title: str
82
+ description: Optional[str] = None
83
+ questions: Dict[str, QuizQuestion]
84
+
85
+ class LectureListItem(BaseModel):
86
+ id: int
87
+ title: str
88
+ courseId: int
89
+ description: Optional[str] = None
90
+
91
+ class Lecture(BaseModel):
92
+ id: int
93
+ courseId: int
94
+ title: str
95
+ description: Optional[str] = None
96
+ content: Optional[str] = None
97
+ videoUrl: Optional[str] = None
98
+ quiz: Optional[Quiz] = None
99
+
100
+ class LectureDetails(Lecture):
101
+ courseName: Optional[str] = None
102
+ courseDescription: Optional[str] = None
103
+ courseLectures: Optional[List[LectureListItem]] = None
104
+
105
+ # Get all courses
106
+ @router.get("/courses", response_model=List[Course])
107
+ async def get_courses(request: Request, auth_token: str = Cookie(None)):
108
+ try:
109
+ # Try to get token from Authorization header if cookie is not present
110
+ if not auth_token:
111
+ auth_header = request.headers.get('Authorization')
112
+ if auth_header and auth_header.startswith('Bearer '):
113
+ auth_token = auth_header.split(' ')[1]
114
+
115
+ if not auth_token:
116
+ raise HTTPException(status_code=401, detail="No authentication token provided")
117
+
118
+ # Verify user is authenticated
119
+ try:
120
+ user_data = decode_token(auth_token)
121
+ except Exception as e:
122
+ print(f"Token decode error: {str(e)}")
123
+ raise HTTPException(status_code=401, detail="Invalid authentication token")
124
+
125
+ conn = connect_db()
126
+ courses = []
127
+
128
+ try:
129
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
130
+ query = """
131
+ SELECT
132
+ c.CourseID as id,
133
+ c.CourseName as name,
134
+ CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
135
+ c.Descriptions as description
136
+ FROM Courses c
137
+ JOIN Instructors i ON c.InstructorID = i.InstructorID
138
+ """
139
+ cursor.execute(query)
140
+ courses = cursor.fetchall()
141
+
142
+ # Get ratings for each course
143
+ for course in courses:
144
+ cursor.execute("""
145
+ SELECT AVG(Rating) as avg_rating, COUNT(*) as count
146
+ FROM Enrollments
147
+ WHERE CourseID = %s AND Rating IS NOT NULL
148
+ """, (course['id'],))
149
+
150
+ rating_data = cursor.fetchone()
151
+ if rating_data and rating_data['avg_rating']:
152
+ course['rating'] = float(rating_data['avg_rating'])
153
+ else:
154
+ course['rating'] = None
155
+
156
+ # Get enrollment count
157
+ cursor.execute("""
158
+ SELECT COUNT(*) as enrolled
159
+ FROM Enrollments
160
+ WHERE CourseID = %s
161
+ """, (course['id'],))
162
+
163
+ enrolled_data = cursor.fetchone()
164
+ if enrolled_data:
165
+ course['enrolled'] = enrolled_data['enrolled']
166
+ else:
167
+ course['enrolled'] = 0
168
+ finally:
169
+ conn.close()
170
+
171
+ return courses
172
+ except Exception as e:
173
+ raise HTTPException(status_code=500, detail=str(e))
174
+
175
+ # Get course details
176
+ @router.get("/courses/{course_id}", response_model=Course)
177
+ async def get_course_details(request: Request, course_id: int, auth_token: str = Cookie(None)):
178
+ try:
179
+ # Try to get token from Authorization header if cookie is not present
180
+ if not auth_token:
181
+ auth_header = request.headers.get('Authorization')
182
+ if auth_header and auth_header.startswith('Bearer '):
183
+ auth_token = auth_header.split(' ')[1]
184
+
185
+ if not auth_token:
186
+ raise HTTPException(status_code=401, detail="No authentication token provided")
187
+
188
+ # Verify user is authenticated
189
+ try:
190
+ user_data = decode_token(auth_token)
191
+ except Exception as e:
192
+ print(f"Token decode error: {str(e)}")
193
+ raise HTTPException(status_code=401, detail="Invalid authentication token")
194
+
195
+ conn = connect_db()
196
+ course = None
197
+
198
+ try:
199
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
200
+ # First check if course exists
201
+ query = """
202
+ SELECT
203
+ c.CourseID as id,
204
+ c.CourseName as name,
205
+ CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
206
+ c.Descriptions as description
207
+ FROM Courses c
208
+ JOIN Instructors i ON c.InstructorID = i.InstructorID
209
+ WHERE c.CourseID = %s
210
+ """
211
+ cursor.execute(query, (course_id,))
212
+ course = cursor.fetchone()
213
+
214
+ if not course:
215
+ raise HTTPException(status_code=404, detail=f"Course with ID {course_id} not found")
216
+
217
+ # Get ratings and enrollment count in one query
218
+ cursor.execute("""
219
+ SELECT
220
+ COUNT(*) as enrolled,
221
+ COALESCE(AVG(Rating), 0) as avg_rating,
222
+ COUNT(CASE WHEN Rating IS NOT NULL THEN 1 END) as rating_count
223
+ FROM Enrollments
224
+ WHERE CourseID = %s
225
+ """, (course_id,))
226
+
227
+ stats = cursor.fetchone()
228
+ if stats:
229
+ course['enrolled'] = stats['enrolled']
230
+ course['rating'] = float(stats['avg_rating']) if stats['rating_count'] > 0 else None
231
+ else:
232
+ course['enrolled'] = 0
233
+ course['rating'] = None
234
+
235
+ finally:
236
+ conn.close()
237
+
238
+ return course
239
+ except HTTPException:
240
+ raise
241
+ except Exception as e:
242
+ print(f"Error in get_course_details: {str(e)}")
243
+ raise HTTPException(status_code=500, detail=str(e))
244
+
245
+ # Get lectures for a course
246
+ @router.get("/courses/{course_id}/lectures", response_model=List[Lecture])
247
+ async def get_course_lectures(request: Request, course_id: int, auth_token: str = Cookie(None)):
248
+ try:
249
+ # Try to get token from Authorization header if cookie is not present
250
+ if not auth_token:
251
+ auth_header = request.headers.get('Authorization')
252
+ if auth_header and auth_header.startswith('Bearer '):
253
+ auth_token = auth_header.split(' ')[1]
254
+
255
+ if not auth_token:
256
+ raise HTTPException(status_code=401, detail="No authentication token provided")
257
+
258
+ try:
259
+ user_data = decode_token(auth_token)
260
+ except Exception as e:
261
+ print(f"Token decode error: {str(e)}")
262
+ raise HTTPException(status_code=401, detail="Invalid authentication token")
263
+
264
+ conn = connect_db()
265
+ lectures = []
266
+
267
+ try:
268
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
269
+ # First verify the course exists
270
+ cursor.execute("SELECT CourseID FROM Courses WHERE CourseID = %s", (course_id,))
271
+ if not cursor.fetchone():
272
+ raise HTTPException(status_code=404, detail="Course not found")
273
+
274
+ query = """
275
+ SELECT
276
+ LectureID as id,
277
+ CourseID as courseId,
278
+ Title as title,
279
+ Description as description
280
+ FROM Lectures
281
+ WHERE CourseID = %s
282
+ ORDER BY LectureID
283
+ """
284
+ cursor.execute(query, (course_id,))
285
+ lectures = cursor.fetchall()
286
+
287
+ # Ensure all fields match the Lecture model
288
+ for lecture in lectures:
289
+ # Ensure the fields exist and have appropriate null values if missing
290
+ if lecture.get('description') is None:
291
+ lecture['description'] = None
292
+ finally:
293
+ conn.close()
294
+
295
+ return lectures
296
+ except HTTPException:
297
+ raise
298
+ except Exception as e:
299
+ print(f"Error in get_course_lectures: {str(e)}")
300
+ raise HTTPException(status_code=500, detail=str(e))
301
+
302
+ # Get lecture details
303
+ @router.get("/lectures/{lecture_id}", response_model=LectureDetails)
304
+ async def get_lecture_details(request: Request, lecture_id: int, auth_token: str = Cookie(None)):
305
+ try:
306
+ # Try to get token from Authorization header if cookie is not present
307
+ if not auth_token:
308
+ auth_header = request.headers.get('Authorization')
309
+ if auth_header and auth_header.startswith('Bearer '):
310
+ auth_token = auth_header.split(' ')[1]
311
+
312
+ if not auth_token:
313
+ raise HTTPException(status_code=401, detail="No authentication token provided")
314
+
315
+ # Verify user is authenticated
316
+ try:
317
+ user_data = decode_token(auth_token)
318
+ except Exception as e:
319
+ print(f"Token decode error: {str(e)}")
320
+ raise HTTPException(status_code=401, detail="Invalid authentication token")
321
+
322
+ conn = connect_db()
323
+ try:
324
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
325
+ # Get lecture details with course data
326
+ query = """
327
+ SELECT
328
+ l.LectureID as id,
329
+ l.CourseID as courseId,
330
+ l.Title as title,
331
+ l.Description as description,
332
+ l.Content as content,
333
+ c.CourseName as courseName,
334
+ c.CourseID,
335
+ c.Descriptions as courseDescription
336
+ FROM Lectures l
337
+ JOIN Courses c ON l.CourseID = c.CourseID
338
+ WHERE l.LectureID = %s
339
+ """
340
+ cursor.execute(query, (lecture_id,))
341
+ lecture = cursor.fetchone()
342
+
343
+ if not lecture:
344
+ raise HTTPException(status_code=404, detail="Lecture not found")
345
+
346
+ # Get course lectures
347
+ cursor.execute("""
348
+ SELECT
349
+ LectureID as id,
350
+ CourseID as courseId,
351
+ Title as title,
352
+ Description as description
353
+ FROM Lectures
354
+ WHERE CourseID = %s
355
+ ORDER BY LectureID
356
+ """, (lecture['courseId'],))
357
+
358
+ course_lectures = cursor.fetchall()
359
+ if not course_lectures:
360
+ course_lectures = []
361
+
362
+ response_data = {
363
+ "id": lecture['id'],
364
+ "courseId": lecture['courseId'],
365
+ "title": lecture['title'],
366
+ "description": lecture['description'],
367
+ "content": lecture['content'],
368
+ "courseName": lecture['courseName'],
369
+ "courseDescription": lecture['courseDescription'],
370
+ "courseLectures": course_lectures,
371
+ "videoUrl": None,
372
+ "quiz": None # Initialize quiz as None
373
+ }
374
+
375
+ # Get video URL if exists
376
+ video_path = f"videos/cid{lecture['courseId']}/lid{lecture['id']}/vid_lecture.mp4"
377
+ try:
378
+ s3.head_object(Bucket="tlhmaterials", Key=video_path)
379
+ response_data['videoUrl'] = f"https://tlhmaterials.s3-{REGION}.amazonaws.com/{video_path}"
380
+ except:
381
+ pass # Keep videoUrl as None if no video exists
382
+
383
+ # Get quiz if exists - fixing this part
384
+ cursor.execute("""
385
+ SELECT q.QuizID, q.Title, q.Description
386
+ FROM Quizzes q
387
+ WHERE q.LectureID = %s
388
+ """, (lecture_id,))
389
+
390
+ quiz_data = cursor.fetchone()
391
+ if quiz_data:
392
+ quiz = {
393
+ "id": quiz_data['QuizID'],
394
+ "title": quiz_data['Title'],
395
+ "description": quiz_data['Description'],
396
+ "questions": {}
397
+ }
398
+
399
+ # Get quiz questions
400
+ cursor.execute("""
401
+ SELECT
402
+ q.QuestionID,
403
+ q.QuestionText,
404
+ o.OptionID,
405
+ o.OptionText,
406
+ o.IsCorrect
407
+ FROM Questions q
408
+ JOIN Options o ON q.QuestionID = o.QuestionID
409
+ WHERE q.QuizID = %s
410
+ ORDER BY q.QuestionID, o.OptionID
411
+ """, (quiz_data['QuizID'],))
412
+
413
+ questions_data = cursor.fetchall()
414
+ current_question_id = None
415
+ current_options = []
416
+ correct_option_index = 0
417
+
418
+ for row in questions_data:
419
+ if current_question_id != row['QuestionID']:
420
+ # Save previous question data
421
+ if current_question_id is not None:
422
+ quiz['questions'][str(current_question_id)] = {
423
+ 'question': question_text,
424
+ 'options': current_options,
425
+ 'correctAnswer': correct_option_index
426
+ }
427
+
428
+ # Start new question
429
+ current_question_id = row['QuestionID']
430
+ question_text = row['QuestionText']
431
+ current_options = []
432
+ correct_option_index = 0
433
+
434
+ current_options.append(row['OptionText'])
435
+ if row['IsCorrect']:
436
+ correct_option_index = len(current_options) - 1
437
+
438
+ # Save the last question
439
+ if current_question_id is not None:
440
+ quiz['questions'][str(current_question_id)] = {
441
+ 'question': question_text,
442
+ 'options': current_options,
443
+ 'correctAnswer': correct_option_index
444
+ }
445
+
446
+ response_data['quiz'] = quiz
447
+
448
+ return response_data
449
+
450
+ finally:
451
+ conn.close()
452
+
453
+ except HTTPException:
454
+ raise
455
+ except Exception as e:
456
+ print(f"Error in get_lecture_details: {str(e)}")
457
+ raise HTTPException(status_code=500, detail=str(e))
458
+
459
+ # Get instructor's courses
460
+ @router.get("/instructor/courses", response_model=List[Course])
461
+ async def get_instructor_courses(
462
+ request: Request,
463
+ auth_token: str = Cookie(None)
464
+ ):
465
+ try:
466
+ # Get token from header if not in cookie
467
+ if not auth_token:
468
+ auth_header = request.headers.get('Authorization')
469
+ if auth_header and auth_header.startswith('Bearer '):
470
+ auth_token = auth_header.split(' ')[1]
471
+ else:
472
+ raise HTTPException(status_code=401, detail="No authentication token provided")
473
+
474
+ # Verify token and get user data
475
+ try:
476
+ user_data = decode_token(auth_token)
477
+ username = user_data['username']
478
+ role = user_data['role']
479
+
480
+ # Verify user is an instructor
481
+ if role != "Instructor":
482
+ raise HTTPException(status_code=403, detail="Only instructors can access this endpoint")
483
+
484
+ # Connect to database
485
+ conn = connect_db()
486
+
487
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
488
+ # Get instructor ID
489
+ cursor.execute("""
490
+ SELECT InstructorID
491
+ FROM Instructors
492
+ WHERE AccountName = %s
493
+ """, (username,))
494
+
495
+ instructor = cursor.fetchone()
496
+ if not instructor:
497
+ raise HTTPException(status_code=404, detail="Instructor not found")
498
+
499
+ instructor_id = instructor['InstructorID']
500
+
501
+ # Get courses by this instructor
502
+ query = """
503
+ SELECT
504
+ c.CourseID as id,
505
+ c.CourseName as name,
506
+ CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
507
+ c.Descriptions as description,
508
+ (SELECT COUNT(*) FROM Enrollments WHERE CourseID = c.CourseID) as enrolled,
509
+ COALESCE(
510
+ (SELECT AVG(Rating)
511
+ FROM Enrollments
512
+ WHERE CourseID = c.CourseID AND Rating IS NOT NULL),
513
+ 0
514
+ ) as rating
515
+ FROM Courses c
516
+ JOIN Instructors i ON c.InstructorID = i.InstructorID
517
+ WHERE c.InstructorID = %s
518
+ ORDER BY c.CourseID DESC
519
+ """
520
+
521
+ cursor.execute(query, (instructor_id,))
522
+ courses = cursor.fetchall()
523
+
524
+ # Format the courses data
525
+ formatted_courses = []
526
+ for course in courses:
527
+ formatted_course = {
528
+ 'id': course['id'],
529
+ 'name': course['name'],
530
+ 'instructor': course['instructor'],
531
+ 'description': course['description'],
532
+ 'enrolled': course['enrolled'],
533
+ 'rating': float(course['rating']) if course['rating'] else None
534
+ }
535
+ formatted_courses.append(formatted_course)
536
+
537
+ return formatted_courses
538
+
539
+ except Exception as e:
540
+ print(f"Database error: {str(e)}")
541
+ raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
542
+
543
+ except HTTPException as he:
544
+ raise he
545
+ except Exception as e:
546
+ print(f"Error fetching instructor courses: {str(e)}")
547
+ raise HTTPException(status_code=500, detail=str(e))
548
+ finally:
549
+ if 'conn' in locals():
550
+ conn.close()
551
+
552
+ # Create a new course
553
+ class CourseCreate(BaseModel):
554
+ name: str
555
+ description: str
556
+ skills: List[str]
557
+ difficulty: str
558
+ duration: Optional[int] = None
559
+
560
+ @router.post("/instructor/courses", response_model=Course)
561
+ async def create_course(
562
+ course_data: CourseCreate,
563
+ request: Request,
564
+ auth_token: str = Cookie(None)
565
+ ):
566
+ try:
567
+ # Get token from header if not in cookie
568
+ if not auth_token:
569
+ auth_header = request.headers.get('Authorization')
570
+ if auth_header and auth_header.startswith('Bearer '):
571
+ auth_token = auth_header.split(' ')[1]
572
+ else:
573
+ raise HTTPException(status_code=401, detail="No authentication token provided")
574
+
575
+ # Verify token and get user data
576
+ try:
577
+ user_data = decode_token(auth_token)
578
+ username = user_data['username']
579
+ role = user_data['role']
580
+
581
+ # Log debugging information
582
+ print(f"POST /instructor/courses - User: {username}, Role: {role}")
583
+ print(f"Course data: {course_data}")
584
+
585
+ # Verify user is an instructor
586
+ if role != "Instructor":
587
+ raise HTTPException(status_code=403, detail="Only instructors can create courses")
588
+
589
+ # Connect to database
590
+ conn = connect_db()
591
+
592
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
593
+ # Get instructor ID
594
+ cursor.execute("""
595
+ SELECT InstructorID
596
+ FROM Instructors
597
+ WHERE AccountName = %s
598
+ """, (username,))
599
+
600
+ instructor = cursor.fetchone()
601
+ if not instructor:
602
+ raise HTTPException(status_code=404, detail="Instructor not found")
603
+
604
+ instructor_id = instructor['InstructorID']
605
+
606
+ # Insert new course
607
+ cursor.execute("""
608
+ INSERT INTO Courses
609
+ (CourseName, Descriptions, Skills, Difficulty, EstimatedDuration, InstructorID)
610
+ VALUES (%s, %s, %s, %s, %s, %s)
611
+ """, (
612
+ course_data.name,
613
+ course_data.description,
614
+ json.dumps(course_data.skills),
615
+ course_data.difficulty,
616
+ course_data.duration or "Self-paced",
617
+ instructor_id
618
+ ))
619
+
620
+ # Get the created course ID
621
+ course_id = cursor.lastrowid
622
+ conn.commit()
623
+
624
+ # Return the created course
625
+ cursor.execute("""
626
+ SELECT
627
+ c.CourseID as id,
628
+ c.CourseName as name,
629
+ CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
630
+ c.Descriptions as description,
631
+ 0 as enrolled,
632
+ NULL as rating
633
+ FROM Courses c
634
+ JOIN Instructors i ON c.InstructorID = i.InstructorID
635
+ WHERE c.CourseID = %s
636
+ """, (course_id,))
637
+
638
+ new_course = cursor.fetchone()
639
+ if not new_course:
640
+ raise HTTPException(status_code=500, detail="Course was created but couldn't be retrieved")
641
+
642
+ return new_course
643
+
644
+ except Exception as e:
645
+ if 'conn' in locals():
646
+ conn.rollback()
647
+ print(f"Database error: {str(e)}")
648
+ raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
649
+
650
+ except HTTPException as he:
651
+ raise he
652
+ except Exception as e:
653
+ print(f"Error creating course: {str(e)}")
654
+ raise HTTPException(status_code=500, detail=str(e))
655
+ finally:
656
+ if 'conn' in locals():
657
+ conn.close()
658
+
659
+ # Enroll in a course
660
+ @router.post("/courses/{course_id}/enroll")
661
+ async def enroll_in_course(
662
+ course_id: int,
663
+ request: Request,
664
+ auth_token: str = Cookie(None)
665
+ ):
666
+ try:
667
+ # Get token from header if not in cookie
668
+ if not auth_token:
669
+ auth_header = request.headers.get('Authorization')
670
+ if auth_header and auth_header.startswith('Bearer '):
671
+ auth_token = auth_header.split(' ')[1]
672
+ else:
673
+ raise HTTPException(status_code=401, detail="No authentication token provided")
674
+
675
+ # Verify token and get user data
676
+ try:
677
+ user_data = decode_token(auth_token)
678
+
679
+ # Get LearnerID from Learners table using the username
680
+ conn = connect_db()
681
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
682
+ cursor.execute("""
683
+ SELECT LearnerID
684
+ FROM Learners
685
+ WHERE AccountName = %s
686
+ """, (user_data['username'],))
687
+ learner = cursor.fetchone()
688
+ if not learner:
689
+ raise HTTPException(status_code=404, detail="Learner not found")
690
+ learner_id = learner['LearnerID']
691
+ except Exception as e:
692
+ print(f"Token/user verification error: {str(e)}")
693
+ raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
694
+
695
+ # Check if the course exists
696
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
697
+ cursor.execute("""
698
+ SELECT CourseID
699
+ FROM Courses
700
+ WHERE CourseID = %s
701
+ """, (course_id,))
702
+ course = cursor.fetchone()
703
+ if not course:
704
+ raise HTTPException(status_code=404, detail="Course not found")
705
+
706
+ # Check if already enrolled
707
+ cursor.execute("""
708
+ SELECT EnrollmentID
709
+ FROM Enrollments
710
+ WHERE LearnerID = %s AND CourseID = %s
711
+ """, (learner_id, course_id))
712
+ existing_enrollment = cursor.fetchone()
713
+ if existing_enrollment:
714
+ return {"message": "Already enrolled in this course"}
715
+
716
+ # Get the actual column names from the Enrollments table
717
+ cursor.execute("DESCRIBE Enrollments")
718
+ columns = cursor.fetchall()
719
+ column_names = [col['Field'] for col in columns]
720
+ print(f"Available columns in Enrollments table: {column_names}")
721
+
722
+ # Enroll the learner in the course with the correct date column
723
+ try:
724
+ # Try different common column names for the enrollment date
725
+ enroll_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
726
+ cursor.execute(
727
+ "CALL sp_EnrollLearner(%s, %s, %s)",
728
+ (learner_id, course_id, enroll_date)
729
+ )
730
+
731
+ conn.commit()
732
+ return {"message": "Successfully enrolled in the course"}
733
+ except Exception as e:
734
+ conn.rollback()
735
+ print(f"Error enrolling in course: {str(e)}")
736
+ raise HTTPException(status_code=500, detail=f"Failed to enroll in course: {str(e)}")
737
+
738
+ except HTTPException as he:
739
+ raise he
740
+ except Exception as e:
741
+ print(f"Error enrolling in course: {str(e)}")
742
+ raise HTTPException(status_code=500, detail=f"Error enrolling in course: {str(e)}")
743
+ finally:
744
+ if 'conn' in locals():
745
+ conn.close()
746
+
747
+ # Get enrolled courses for the current user
748
+ @router.get("/user/courses", response_model=List[Course])
749
+ async def get_enrolled_courses(
750
+ request: Request,
751
+ auth_token: str = Cookie(None)
752
+ ):
753
+ try:
754
+ # Get token from header if not in cookie
755
+ if not auth_token:
756
+ auth_header = request.headers.get('Authorization')
757
+ if auth_header and auth_header.startswith('Bearer '):
758
+ auth_token = auth_header.split(' ')[1]
759
+ else:
760
+ raise HTTPException(status_code=401, detail="No authentication token provided")
761
+
762
+ # Verify token and get user data
763
+ try:
764
+ user_data = decode_token(auth_token)
765
+
766
+ # Get LearnerID from Learners table using the username
767
+ conn = connect_db()
768
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
769
+ cursor.execute("""
770
+ SELECT LearnerID
771
+ FROM Learners
772
+ WHERE AccountName = %s
773
+ """, (user_data['username'],))
774
+ learner = cursor.fetchone()
775
+ if not learner:
776
+ raise HTTPException(status_code=404, detail="Learner not found")
777
+ learner_id = learner['LearnerID']
778
+ except Exception as e:
779
+ print(f"Token/user verification error: {str(e)}")
780
+ raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
781
+
782
+ # Get enrolled courses
783
+ courses = []
784
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
785
+ query = """
786
+ SELECT
787
+ c.CourseID as id,
788
+ c.CourseName as name,
789
+ CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
790
+ c.Descriptions as description
791
+ FROM Courses c
792
+ JOIN Instructors i ON c.InstructorID = i.InstructorID
793
+ JOIN Enrollments e ON c.CourseID = e.CourseID
794
+ WHERE e.LearnerID = %s
795
+ """
796
+ cursor.execute(query, (learner_id,))
797
+ courses = cursor.fetchall()
798
+
799
+ # Get ratings and enrollment count for each course
800
+ for course in courses:
801
+ cursor.execute("""
802
+ SELECT AVG(Rating) as avg_rating, COUNT(*) as count
803
+ FROM Enrollments
804
+ WHERE CourseID = %s AND Rating IS NOT NULL
805
+ """, (course['id'],))
806
+
807
+ rating_data = cursor.fetchone()
808
+ if rating_data and rating_data['avg_rating']:
809
+ course['rating'] = float(rating_data['avg_rating'])
810
+ else:
811
+ course['rating'] = None
812
+
813
+ # Get enrollment count
814
+ cursor.execute("""
815
+ SELECT COUNT(*) as enrolled
816
+ FROM Enrollments
817
+ WHERE CourseID = %s
818
+ """, (course['id'],))
819
+
820
+ enrolled_data = cursor.fetchone()
821
+ if enrolled_data:
822
+ course['enrolled'] = enrolled_data['enrolled']
823
+ else:
824
+ course['enrolled'] = 0
825
+
826
+ return courses
827
+
828
+ except HTTPException as he:
829
+ raise he
830
+ except Exception as e:
831
+ print(f"Error fetching enrolled courses: {str(e)}")
832
+ raise HTTPException(status_code=500, detail=f"Error fetching enrolled courses: {str(e)}")
833
+ finally:
834
+ if 'conn' in locals():
835
+ conn.close()
836
+
837
+ # Get user profile
838
+ @router.get("/user/profile")
839
+ async def get_user_profile(
840
+ request: Request,
841
+ auth_token: str = Cookie(None)
842
+ ):
843
+ try:
844
+ # Get token from header if not in cookie
845
+ if not auth_token:
846
+ auth_header = request.headers.get('Authorization')
847
+ if auth_header and auth_header.startswith('Bearer '):
848
+ auth_token = auth_header.split(' ')[1]
849
+ else:
850
+ raise HTTPException(status_code=401, detail="No authentication token provided")
851
+
852
+ # Verify token and get user data
853
+ try:
854
+ user_data = decode_token(auth_token)
855
+ username = user_data['username']
856
+ role = user_data['role']
857
+
858
+ # Connect to database
859
+ conn = connect_db()
860
+
861
+ # Get user information based on role
862
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
863
+ if role == "Learner":
864
+ cursor.execute("""
865
+ SELECT
866
+ LearnerName as name,
867
+ Email as email,
868
+ PhoneNumber as phoneNumber
869
+ FROM Learners
870
+ WHERE AccountName = %s
871
+ """, (username,))
872
+ user_info = cursor.fetchone()
873
+
874
+ if not user_info:
875
+ raise HTTPException(status_code=404, detail="Learner not found")
876
+
877
+ elif role == "Instructor":
878
+ cursor.execute("""
879
+ SELECT
880
+ InstructorName as name,
881
+ Email as email,
882
+ Expertise as expertise
883
+ FROM Instructors
884
+ WHERE AccountName = %s
885
+ """, (username,))
886
+ user_info = cursor.fetchone()
887
+
888
+ if not user_info:
889
+ raise HTTPException(status_code=404, detail="Instructor not found")
890
+ else:
891
+ raise HTTPException(status_code=403, detail="Invalid user role")
892
+
893
+ # Add role and username to the response
894
+ user_info['role'] = role
895
+ user_info['username'] = username
896
+
897
+ return user_info
898
+
899
+ except Exception as e:
900
+ print(f"Token/user verification error: {str(e)}")
901
+ raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
902
+ except HTTPException as he:
903
+ raise he
904
+ except Exception as e:
905
+ print(f"Error fetching user profile: {str(e)}")
906
+ raise HTTPException(status_code=500, detail=f"Error fetching user profile: {str(e)}")
907
+ finally:
908
+ if 'conn' in locals():
909
+ conn.close()
910
+
911
+ # Update user profile
912
+ @router.put("/user/profile")
913
+ async def update_user_profile(
914
+ request: Request,
915
+ auth_token: str = Cookie(None)
916
+ ):
917
+ try:
918
+ # Get token from header if not in cookie
919
+ if not auth_token:
920
+ auth_header = request.headers.get('Authorization')
921
+ if auth_header and auth_header.startswith('Bearer '):
922
+ auth_token = auth_header.split(' ')[1]
923
+ else:
924
+ raise HTTPException(status_code=401, detail="No authentication token provided")
925
+
926
+ # Verify token and get user data
927
+ try:
928
+ user_data = decode_token(auth_token)
929
+ username = user_data['username']
930
+ role = user_data['role']
931
+
932
+ # Get request body
933
+ profile_data = await request.json()
934
+
935
+ # Connect to database
936
+ conn = connect_db()
937
+
938
+ # Update user information based on role
939
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
940
+ if role == "Learner":
941
+ # Prepare update fields
942
+ update_fields = []
943
+ params = []
944
+
945
+ if 'name' in profile_data:
946
+ update_fields.append("LearnerName = %s")
947
+ params.append(profile_data['name'])
948
+
949
+ if 'email' in profile_data:
950
+ update_fields.append("Email = %s")
951
+ params.append(profile_data['email'])
952
+
953
+ if 'phoneNumber' in profile_data:
954
+ update_fields.append("PhoneNumber = %s")
955
+ params.append(profile_data['phoneNumber'])
956
+
957
+ if not update_fields:
958
+ return {"message": "No fields to update"}
959
+
960
+ # Add username to params
961
+ params.append(username)
962
+
963
+ # Construct and execute SQL
964
+ sql = f"""
965
+ UPDATE Learners
966
+ SET {', '.join(update_fields)}
967
+ WHERE AccountName = %s
968
+ """
969
+ cursor.execute(sql, params)
970
+
971
+ elif role == "Instructor":
972
+ # Prepare update fields
973
+ update_fields = []
974
+ params = []
975
+
976
+ if 'name' in profile_data:
977
+ update_fields.append("InstructorName = %s")
978
+ params.append(profile_data['name'])
979
+
980
+ if 'email' in profile_data:
981
+ update_fields.append("Email = %s")
982
+ params.append(profile_data['email'])
983
+
984
+ if 'expertise' in profile_data:
985
+ update_fields.append("Expertise = %s")
986
+ params.append(profile_data['expertise'])
987
+
988
+ if not update_fields:
989
+ return {"message": "No fields to update"}
990
+
991
+ # Add username to params
992
+ params.append(username)
993
+
994
+ # Construct and execute SQL
995
+ sql = f"""
996
+ UPDATE Instructors
997
+ SET {', '.join(update_fields)}
998
+ WHERE AccountName = %s
999
+ """
1000
+ cursor.execute(sql, params)
1001
+
1002
+ else:
1003
+ raise HTTPException(status_code=403, detail="Invalid user role")
1004
+
1005
+ conn.commit()
1006
+ return {"message": "Profile updated successfully"}
1007
+
1008
+ except Exception as e:
1009
+ print(f"Token/user verification error: {str(e)}")
1010
+ raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
1011
+ except HTTPException as he:
1012
+ raise he
1013
+ except Exception as e:
1014
+ print(f"Error updating user profile: {str(e)}")
1015
+ raise HTTPException(status_code=500, detail=f"Error updating user profile: {str(e)}")
1016
+ finally:
1017
+ if 'conn' in locals():
1018
+ conn.close()
1019
+
1020
+ # Get dashboard data for the current user
1021
+ @router.get("/user/dashboard")
1022
+ async def get_user_dashboard(
1023
+ request: Request,
1024
+ auth_token: str = Cookie(None)
1025
+ ):
1026
+ try:
1027
+ # Get token from header if not in cookie
1028
+ if not auth_token:
1029
+ auth_header = request.headers.get('Authorization')
1030
+ if auth_header and auth_header.startswith('Bearer '):
1031
+ auth_token = auth_header.split(' ')[1]
1032
+ else:
1033
+ raise HTTPException(status_code=401, detail="No authentication token provided")
1034
+
1035
+ # Verify token and get user data
1036
+ try:
1037
+ user_data = decode_token(auth_token)
1038
+
1039
+ # Get LearnerID from Learners table using the username
1040
+ conn = connect_db()
1041
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
1042
+ cursor.execute("""
1043
+ SELECT LearnerID, LearnerName
1044
+ FROM Learners
1045
+ WHERE AccountName = %s
1046
+ """, (user_data['username'],))
1047
+ learner = cursor.fetchone()
1048
+ if not learner:
1049
+ raise HTTPException(status_code=404, detail="Learner not found")
1050
+ learner_id = learner['LearnerID']
1051
+ learner_name = learner['LearnerName']
1052
+ except Exception as e:
1053
+ print(f"Token/user verification error: {str(e)}")
1054
+ raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
1055
+
1056
+ # Calculate dashboard metrics
1057
+ dashboard_data = {
1058
+ "learnerName": learner_name,
1059
+ "enrolled": 0,
1060
+ "completed": 0,
1061
+ "completionRate": "0%",
1062
+ "lecturesPassed": 0,
1063
+ "statistics": {
1064
+ "lecturesPassed": [],
1065
+ "averageScores": []
1066
+ },
1067
+ "enrolledCourses": []
1068
+ }
1069
+
1070
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
1071
+ # Get enrollment count
1072
+ cursor.execute("SELECT COUNT(*) as count FROM Enrollments WHERE LearnerID = %s", (learner_id,))
1073
+ enrolled_data = cursor.fetchone()
1074
+ dashboard_data["enrolled"] = enrolled_data['count'] if enrolled_data else 0
1075
+
1076
+ # Get completed courses count
1077
+ cursor.execute(
1078
+ "SELECT COUNT(*) as count FROM Enrollments WHERE LearnerID = %s AND Percentage = 100",
1079
+ (learner_id,)
1080
+ )
1081
+ completed_data = cursor.fetchone()
1082
+ completed = completed_data['count'] if completed_data else 0
1083
+ dashboard_data["completed"] = completed
1084
+
1085
+ # Calculate completion rate
1086
+ if dashboard_data["enrolled"] > 0:
1087
+ rate = (completed / dashboard_data["enrolled"]) * 100
1088
+ dashboard_data["completionRate"] = f"{rate:.1f}%"
1089
+
1090
+ # Get passed lectures count
1091
+ cursor.execute(
1092
+ "SELECT COUNT(*) as count FROM LectureResults WHERE LearnerID = %s AND State = 'passed'",
1093
+ (learner_id,)
1094
+ )
1095
+ passed_data = cursor.fetchone()
1096
+ dashboard_data["lecturesPassed"] = passed_data['count'] if passed_data else 0
1097
+
1098
+ # Get statistics data - passed lectures over time
1099
+ cursor.execute("""
1100
+ SELECT Date, Score,
1101
+ DATE_FORMAT(Date, '%%Y-%%m-%%d') as formatted_date
1102
+ FROM LectureResults
1103
+ WHERE LearnerID = %s AND State = 'passed'
1104
+ ORDER BY Date
1105
+ """, (learner_id,))
1106
+
1107
+ stats_data = cursor.fetchall()
1108
+ date_groups = {}
1109
+ score_groups = {}
1110
+
1111
+ for row in stats_data:
1112
+ date_str = row['formatted_date']
1113
+ if date_str not in date_groups:
1114
+ date_groups[date_str] = 0
1115
+ date_groups[date_str] += 1
1116
+
1117
+ if date_str not in score_groups:
1118
+ score_groups[date_str] = {"total": 0, "count": 0}
1119
+ score_groups[date_str]["total"] += row['Score']
1120
+ score_groups[date_str]["count"] += 1
1121
+
1122
+ # Format the statistics data
1123
+ for date_str in date_groups:
1124
+ dashboard_data["statistics"]["lecturesPassed"].append({
1125
+ "date": date_str,
1126
+ "count": date_groups[date_str]
1127
+ })
1128
+
1129
+ avg_score = score_groups[date_str]["total"] / score_groups[date_str]["count"]
1130
+ dashboard_data["statistics"]["averageScores"].append({
1131
+ "date": date_str,
1132
+ "score": round(avg_score, 2)
1133
+ })
1134
+
1135
+ # Get enrolled courses with percentage
1136
+ cursor.execute("""
1137
+ SELECT
1138
+ c.CourseID as id,
1139
+ c.CourseName as name,
1140
+ CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
1141
+ c.Descriptions as description,
1142
+ e.Percentage as percentage
1143
+ FROM Courses c
1144
+ JOIN Instructors i ON c.InstructorID = i.InstructorID
1145
+ JOIN Enrollments e ON c.CourseID = e.CourseID
1146
+ WHERE e.LearnerID = %s
1147
+ """, (learner_id,))
1148
+
1149
+ courses = cursor.fetchall()
1150
+ dashboard_data["enrolledCourses"] = courses
1151
+
1152
+ return dashboard_data
1153
+
1154
+ except HTTPException as he:
1155
+ raise he
1156
+ except Exception as e:
1157
+ print(f"Error fetching dashboard data: {str(e)}")
1158
+ raise HTTPException(status_code=500, detail=f"Error fetching dashboard data: {str(e)}")
1159
+ finally:
1160
+ if 'conn' in locals():
1161
+ conn.close()
1162
+
1163
+ # Get dashboard data for instructor
1164
+ @router.get("/instructor/dashboard")
1165
+ async def get_instructor_dashboard(
1166
+ request: Request,
1167
+ auth_token: str = Cookie(None)
1168
+ ):
1169
+ try:
1170
+ # Get token from header if not in cookie
1171
+ if not auth_token:
1172
+ auth_header = request.headers.get('Authorization')
1173
+ if auth_header and auth_header.startswith('Bearer '):
1174
+ auth_token = auth_header.split(' ')[1]
1175
+ else:
1176
+ raise HTTPException(status_code=401, detail="No authentication token provided")
1177
+
1178
+ # Verify token and get user data
1179
+ try:
1180
+ user_data = decode_token(auth_token)
1181
+ username = user_data.get('username')
1182
+ role = user_data.get('role')
1183
+
1184
+ if not username or not role:
1185
+ raise HTTPException(status_code=401, detail="Invalid token data")
1186
+
1187
+ # Verify user is an instructor
1188
+ if role != "Instructor":
1189
+ raise HTTPException(status_code=403, detail="Only instructors can access this endpoint")
1190
+
1191
+ # Connect to database
1192
+ conn = connect_db()
1193
+
1194
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
1195
+ # Get instructor ID
1196
+ try:
1197
+ cursor.execute("""
1198
+ SELECT InstructorID
1199
+ FROM Instructors
1200
+ WHERE AccountName = %s
1201
+ """, (username,))
1202
+
1203
+ instructor = cursor.fetchone()
1204
+ if not instructor:
1205
+ raise HTTPException(status_code=404, detail="Instructor not found")
1206
+
1207
+ instructor_id = instructor['InstructorID']
1208
+ except Exception as e:
1209
+ print(f"Error getting instructor ID: {str(e)}")
1210
+ raise HTTPException(status_code=500, detail="Error retrieving instructor information")
1211
+
1212
+ try:
1213
+ # Get total courses
1214
+ cursor.execute("""
1215
+ SELECT COUNT(*) as total_courses
1216
+ FROM Courses
1217
+ WHERE InstructorID = %s
1218
+ """, (instructor_id,))
1219
+ total_courses = cursor.fetchone()['total_courses']
1220
+
1221
+ # Get total students and stats
1222
+ cursor.execute("""
1223
+ SELECT
1224
+ COUNT(DISTINCT e.LearnerID) as total_students,
1225
+ COALESCE(AVG(e.Rating), 0) as average_rating,
1226
+ COUNT(*) as total_enrollments,
1227
+ SUM(CASE WHEN e.Percentage = 100 THEN 1 ELSE 0 END) as completed_enrollments
1228
+ FROM Courses c
1229
+ LEFT JOIN Enrollments e ON c.CourseID = e.CourseID
1230
+ WHERE c.InstructorID = %s
1231
+ """, (instructor_id,))
1232
+
1233
+ stats = cursor.fetchone()
1234
+ if not stats:
1235
+ stats = {
1236
+ 'total_students': 0,
1237
+ 'average_rating': 0,
1238
+ 'total_enrollments': 0,
1239
+ 'completed_enrollments': 0
1240
+ }
1241
+
1242
+ completion_rate = round((stats['completed_enrollments'] / stats['total_enrollments'] * 100)
1243
+ if stats['total_enrollments'] > 0 else 0, 1)
1244
+
1245
+ # Get student growth
1246
+ cursor.execute("""
1247
+ SELECT
1248
+ DATE_FORMAT(EnrollmentDate, '%%Y-%%m') as month,
1249
+ COUNT(DISTINCT LearnerID) as students
1250
+ FROM Courses c
1251
+ JOIN Enrollments e ON c.CourseID = e.CourseID
1252
+ WHERE c.InstructorID = %s
1253
+ AND EnrollmentDate >= DATE_SUB(CURRENT_DATE, INTERVAL 2 MONTH)
1254
+ GROUP BY DATE_FORMAT(EnrollmentDate, '%%Y-%%m')
1255
+ ORDER BY month DESC
1256
+ LIMIT 2
1257
+ """, (instructor_id,))
1258
+
1259
+ growth_data = cursor.fetchall()
1260
+
1261
+ current_month = growth_data[0]['students'] if growth_data else 0
1262
+ prev_month = growth_data[1]['students'] if len(growth_data) > 1 else 0
1263
+ student_growth = round(((current_month - prev_month) / prev_month * 100)
1264
+ if prev_month > 0 else 0, 1)
1265
+
1266
+ # Get courses
1267
+ cursor.execute("""
1268
+ SELECT
1269
+ c.CourseID as id,
1270
+ c.CourseName as name,
1271
+ c.Descriptions as description,
1272
+ c.AverageRating as rating,
1273
+ COUNT(DISTINCT e.LearnerID) as enrollments,
1274
+ AVG(e.Percentage) as completionRate
1275
+ FROM Courses c
1276
+ LEFT JOIN Enrollments e ON c.CourseID = e.CourseID
1277
+ WHERE c.InstructorID = %s
1278
+ GROUP BY c.CourseID, c.CourseName, c.Descriptions, c.AverageRating
1279
+ ORDER BY c.CreatedAt DESC
1280
+ """, (instructor_id,))
1281
+
1282
+ courses = cursor.fetchall() or []
1283
+
1284
+ # Get enrollment trends
1285
+ cursor.execute("""
1286
+ SELECT
1287
+ DATE_FORMAT(e.EnrollmentDate, '%%Y-%%m-%%d') as date,
1288
+ COUNT(*) as value
1289
+ FROM Courses c
1290
+ JOIN Enrollments e ON c.CourseID = e.CourseID
1291
+ WHERE c.InstructorID = %s
1292
+ AND e.EnrollmentDate >= DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY)
1293
+ GROUP BY DATE_FORMAT(e.EnrollmentDate, '%%Y-%%m-%%d')
1294
+ ORDER BY date
1295
+ """, (instructor_id,))
1296
+
1297
+ enrollment_trends = cursor.fetchall() or []
1298
+
1299
+ # Get rating trends
1300
+ cursor.execute("""
1301
+ SELECT
1302
+ DATE_FORMAT(e.EnrollmentDate, '%%Y-%%m-%%d') as date,
1303
+ AVG(e.Rating) as value
1304
+ FROM Courses c
1305
+ JOIN Enrollments e ON c.CourseID = e.CourseID
1306
+ WHERE c.InstructorID = %s
1307
+ AND e.EnrollmentDate >= DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY)
1308
+ AND e.Rating IS NOT NULL
1309
+ GROUP BY DATE_FORMAT(e.EnrollmentDate, '%%Y-%%m-%%d')
1310
+ ORDER BY date
1311
+ """, (instructor_id,))
1312
+
1313
+ rating_trends = cursor.fetchall() or []
1314
+
1315
+ # Format the data
1316
+ formatted_courses = [
1317
+ {
1318
+ 'id': course['id'],
1319
+ 'name': course['name'],
1320
+ 'description': course['description'] or "",
1321
+ 'enrollments': course['enrollments'] or 0,
1322
+ 'rating': round(float(course['rating']), 1) if course['rating'] else 0.0,
1323
+ 'completionRate': round(float(course['completionRate']), 1) if course['completionRate'] else 0.0
1324
+ } for course in courses
1325
+ ]
1326
+
1327
+ formatted_enrollment_trends = [
1328
+ {
1329
+ 'date': trend['date'],
1330
+ 'value': int(trend['value'])
1331
+ } for trend in enrollment_trends
1332
+ ]
1333
+
1334
+ formatted_rating_trends = [
1335
+ {
1336
+ 'date': trend['date'],
1337
+ 'value': round(float(trend['value']), 1)
1338
+ } for trend in rating_trends if trend['value'] is not None
1339
+ ]
1340
+
1341
+ dashboard_data = {
1342
+ "metrics": {
1343
+ "totalCourses": total_courses,
1344
+ "totalStudents": stats['total_students'],
1345
+ "averageRating": round(float(stats['average_rating']), 1),
1346
+ "completionRate": completion_rate,
1347
+ "studentGrowth": student_growth
1348
+ },
1349
+ "courses": formatted_courses,
1350
+ "enrollmentTrends": formatted_enrollment_trends,
1351
+ "ratingTrends": formatted_rating_trends,
1352
+ "courseEnrollments": [
1353
+ {
1354
+ "courseName": course['name'],
1355
+ "enrollments": course['enrollments'] or 0
1356
+ } for course in courses
1357
+ ]
1358
+ }
1359
+
1360
+ return dashboard_data
1361
+
1362
+ except Exception as e:
1363
+ print(f"Error processing dashboard data: {str(e)}")
1364
+ raise HTTPException(status_code=500, detail="Error processing dashboard data")
1365
+
1366
+ except HTTPException as he:
1367
+ raise he
1368
+ except Exception as e:
1369
+ print(f"Database error: {str(e)}")
1370
+ raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
1371
+
1372
+ except HTTPException as he:
1373
+ raise he
1374
+ except Exception as e:
1375
+ print(f"Error fetching instructor dashboard: {str(e)}")
1376
+ raise HTTPException(status_code=500, detail=str(e))
1377
+ finally:
1378
+ if 'conn' in locals():
1379
+ conn.close()
1380
+
1381
+ # Quiz submission model
1382
+ class QuizSubmission(BaseModel):
1383
+ answers: dict[int, str] # questionId -> selected answer text
1384
+
1385
+ @router.post("/lectures/{lecture_id}/quiz/submit")
1386
+ async def submit_quiz_answers(
1387
+ lecture_id: int,
1388
+ submission: QuizSubmission,
1389
+ request: Request,
1390
+ auth_token: str = Cookie(None)
1391
+ ):
1392
+ try:
1393
+ # Get token from header if not in cookie
1394
+ if not auth_token:
1395
+ auth_header = request.headers.get('Authorization')
1396
+ if auth_header and auth_header.startswith('Bearer '):
1397
+ auth_token = auth_header.split(' ')[1]
1398
+ else:
1399
+ raise HTTPException(status_code=401, detail="No authentication token provided")
1400
+
1401
+ # Verify token and get user data
1402
+ try:
1403
+ user_data = decode_token(auth_token)
1404
+ # Get LearnerID from Learners table using the username
1405
+ conn = connect_db()
1406
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
1407
+ cursor.execute("""
1408
+ SELECT LearnerID
1409
+ FROM Learners
1410
+ WHERE AccountName = %s
1411
+ """, (user_data['username'],))
1412
+ learner = cursor.fetchone()
1413
+ if not learner:
1414
+ raise HTTPException(status_code=404, detail="Learner not found")
1415
+ learner_id = learner['LearnerID']
1416
+ except Exception as e:
1417
+ print(f"Token/user verification error: {str(e)}")
1418
+ raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
1419
+
1420
+ try:
1421
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
1422
+ # First verify the quiz exists for this lecture
1423
+ cursor.execute("""
1424
+ SELECT QuizID
1425
+ FROM Quizzes
1426
+ WHERE LectureID = %s
1427
+ """, (lecture_id,))
1428
+ quiz = cursor.fetchone()
1429
+ if not quiz:
1430
+ raise HTTPException(status_code=404, detail="Quiz not found for this lecture")
1431
+
1432
+ quiz_id = quiz['QuizID']
1433
+
1434
+ # Get correct answers for validation
1435
+ cursor.execute("""
1436
+ SELECT q.QuestionID, o.OptionText
1437
+ FROM Questions q
1438
+ JOIN Options o ON q.QuestionID = o.QuestionID
1439
+ WHERE q.QuizID = %s AND o.IsCorrect = 1
1440
+ """, (quiz_id,))
1441
+
1442
+ correct_answers = {row['QuestionID']: row['OptionText'] for row in cursor.fetchall()}
1443
+
1444
+ # Calculate score
1445
+ total_questions = len(correct_answers)
1446
+ if total_questions == 0:
1447
+ raise HTTPException(status_code=500, detail="No questions found for this quiz")
1448
+
1449
+ correct_count = sum(
1450
+ 1 for q_id, answer in submission.answers.items()
1451
+ if str(q_id) in map(str, correct_answers.keys()) and answer == correct_answers[int(q_id)]
1452
+ )
1453
+
1454
+ score = (correct_count / total_questions) * 100
1455
+
1456
+ # Get the CourseID for this lecture
1457
+ cursor.execute("""
1458
+ SELECT CourseID
1459
+ FROM Lectures
1460
+ WHERE LectureID = %s
1461
+ """, (lecture_id,))
1462
+ lecture_data = cursor.fetchone()
1463
+ if not lecture_data:
1464
+ raise HTTPException(status_code=404, detail="Lecture not found")
1465
+
1466
+ course_id = lecture_data['CourseID']
1467
+
1468
+ # Save or update the score using direct SQL instead of stored procedure
1469
+ try:
1470
+ # Use stored procedure to update or insert the lecture result
1471
+ cursor.execute(
1472
+ "CALL sp_update_lecture_result(%s, %s, %s, %s)",
1473
+ (learner_id, course_id, lecture_id, score)
1474
+ )
1475
+
1476
+ # Update course completion percentage
1477
+ try:
1478
+ # Get total lectures in the course
1479
+ cursor.execute("""
1480
+ SELECT COUNT(*) as total_lectures
1481
+ FROM Lectures
1482
+ WHERE CourseID = %s
1483
+ """, (course_id,))
1484
+ total_lectures = cursor.fetchone()['total_lectures']
1485
+
1486
+ # Get passed lectures
1487
+ cursor.execute("""
1488
+ SELECT COUNT(*) as passed_lectures
1489
+ FROM LectureResults
1490
+ WHERE LearnerID = %s AND CourseID = %s AND State = 'passed'
1491
+ """, (learner_id, course_id))
1492
+ passed_lectures = cursor.fetchone()['passed_lectures']
1493
+
1494
+ # Calculate percentage
1495
+ if total_lectures > 0:
1496
+ percentage_raw = (passed_lectures * 100.0) / total_lectures
1497
+
1498
+ # Convert to percentage scale
1499
+ if percentage_raw < 10:
1500
+ percentage = 0
1501
+ elif percentage_raw < 30:
1502
+ percentage = 20
1503
+ elif percentage_raw < 50:
1504
+ percentage = 40
1505
+ elif percentage_raw < 70:
1506
+ percentage = 60
1507
+ elif percentage_raw < 90:
1508
+ percentage = 80
1509
+ else:
1510
+ percentage = 100
1511
+
1512
+ # Update enrollment record
1513
+ cursor.execute("""
1514
+ UPDATE Enrollments
1515
+ SET Percentage = %s
1516
+ WHERE LearnerID = %s AND CourseID = %s
1517
+ """, (percentage, learner_id, course_id))
1518
+ except Exception as e:
1519
+ print(f"Error updating course percentage: {str(e)}")
1520
+ # Continue even if percentage update fails
1521
+
1522
+ conn.commit()
1523
+ print(f"Score updated successfully for learner {learner_id}, lecture {lecture_id}")
1524
+ except Exception as e:
1525
+ print(f"Error saving quiz score: {str(e)}")
1526
+ conn.rollback()
1527
+ raise HTTPException(status_code=500, detail=f"Failed to save quiz score: {str(e)}")
1528
+
1529
+ return {
1530
+ "score": score,
1531
+ "total_questions": total_questions,
1532
+ "correct_answers": correct_count
1533
+ }
1534
+
1535
+ finally:
1536
+ conn.close()
1537
+
1538
+ except HTTPException as he:
1539
+ raise he
1540
+ except Exception as e:
1541
+ print(f"Error submitting quiz: {str(e)}")
1542
+ raise HTTPException(status_code=500, detail=f"Error submitting quiz: {str(e)}")
1543
+
1544
+ @router.get("/lectures/{lecture_id}/quiz/results")
1545
+ async def get_quiz_results(
1546
+ lecture_id: int,
1547
+ request: Request,
1548
+ auth_token: str = Cookie(None)
1549
+ ):
1550
+ try:
1551
+ # Verify auth token
1552
+ if not auth_token:
1553
+ auth_header = request.headers.get('Authorization')
1554
+ if auth_header and auth_header.startswith('Bearer '):
1555
+ auth_token = auth_header.split(' ')[1]
1556
+
1557
+ if not auth_token:
1558
+ raise HTTPException(status_code=401, detail="No authentication token provided")
1559
+
1560
+ try:
1561
+ user_data = decode_token(auth_token)
1562
+ except Exception as e:
1563
+ print(f"Token decode error: {str(e)}")
1564
+ raise HTTPException(status_code=401, detail="Invalid authentication token")
1565
+
1566
+ conn = connect_db()
1567
+ try:
1568
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
1569
+ cursor.execute("""
1570
+ SELECT Score, State, Date
1571
+ FROM LectureResults
1572
+ WHERE LearnerID = %s AND LectureID = %s
1573
+ ORDER BY Date DESC
1574
+ LIMIT 1
1575
+ """, (user_data["id"], lecture_id))
1576
+
1577
+ result = cursor.fetchone()
1578
+ if not result:
1579
+ return None
1580
+
1581
+ return {
1582
+ "score": float(result["Score"]),
1583
+ "status": result["State"],
1584
+ "date": result["Date"].isoformat()
1585
+ }
1586
+
1587
+ finally:
1588
+ conn.close()
1589
+
1590
+ except HTTPException:
1591
+ raise
1592
+ except Exception as e:
1593
+ print(f"Error in get_quiz_results: {str(e)}")
1594
+ raise HTTPException(status_code=500, detail=str(e))
1595
+
1596
+ # Test endpoints for debugging route registration
1597
+ @router.get("/instructor/courses/test")
1598
+ async def test_instructor_courses_get():
1599
+ return {"message": "GET /instructor/courses/test works"}
1600
+
1601
+ @router.post("/instructor/courses/test")
1602
+ async def test_instructor_courses_post():
1603
+ return {"message": "POST /instructor/courses/test works"}
1604
+
1605
+ # Get instructor course details
1606
+ @router.get("/instructor/courses/{course_id}", response_model=Course)
1607
+ async def get_instructor_course_details(
1608
+ request: Request,
1609
+ course_id: int,
1610
+ auth_token: str = Cookie(None)
1611
+ ):
1612
+ try:
1613
+ # Get token from header if not in cookie
1614
+ if not auth_token:
1615
+ auth_header = request.headers.get('Authorization')
1616
+ if auth_header and auth_header.startswith('Bearer '):
1617
+ auth_token = auth_header.split(' ')[1]
1618
+ else:
1619
+ raise HTTPException(status_code=401, detail="No authentication token provided")
1620
+
1621
+ # Verify token and get user data
1622
+ try:
1623
+ user_data = decode_token(auth_token)
1624
+ username = user_data['username']
1625
+ role = user_data['role']
1626
+
1627
+ # Verify user is an instructor
1628
+ if role != "Instructor":
1629
+ raise HTTPException(status_code=403, detail="Only instructors can access this endpoint")
1630
+
1631
+ # Connect to database
1632
+ conn = connect_db()
1633
+
1634
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
1635
+ # Get instructor ID
1636
+ cursor.execute("""
1637
+ SELECT InstructorID
1638
+ FROM Instructors
1639
+ WHERE AccountName = %s
1640
+ """, (username,))
1641
+
1642
+ instructor = cursor.fetchone()
1643
+ if not instructor:
1644
+ raise HTTPException(status_code=404, detail="Instructor not found")
1645
+
1646
+ instructor_id = instructor['InstructorID']
1647
+
1648
+ # Get course details, ensuring it belongs to this instructor
1649
+ query = """
1650
+ SELECT
1651
+ c.CourseID as id,
1652
+ c.CourseName as name,
1653
+ CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
1654
+ c.Descriptions as description,
1655
+ (SELECT COUNT(*) FROM Enrollments WHERE CourseID = c.CourseID) as enrolled,
1656
+ COALESCE(
1657
+ (SELECT AVG(Rating)
1658
+ FROM Enrollments
1659
+ WHERE CourseID = c.CourseID AND Rating IS NOT NULL),
1660
+ 0
1661
+ ) as rating,
1662
+ c.Skills as skills,
1663
+ c.Difficulty as difficulty,
1664
+ c.EstimatedDuration as duration
1665
+ FROM Courses c
1666
+ JOIN Instructors i ON c.InstructorID = i.InstructorID
1667
+ WHERE c.CourseID = %s AND c.InstructorID = %s
1668
+ """
1669
+
1670
+ cursor.execute(query, (course_id, instructor_id))
1671
+ course = cursor.fetchone()
1672
+
1673
+ if not course:
1674
+ raise HTTPException(status_code=404, detail=f"Course with ID {course_id} not found or not owned by this instructor")
1675
+
1676
+ # Format the course data
1677
+ if course['rating']:
1678
+ course['rating'] = float(course['rating'])
1679
+
1680
+ # Convert skills from JSON string if needed
1681
+ if isinstance(course.get('skills'), str):
1682
+ try:
1683
+ course['skills'] = json.loads(course['skills'])
1684
+ except:
1685
+ course['skills'] = []
1686
+
1687
+ return course
1688
+
1689
+ except Exception as e:
1690
+ print(f"Database error: {str(e)}")
1691
+ raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
1692
+
1693
+ except HTTPException as he:
1694
+ raise he
1695
+ except Exception as e:
1696
+ print(f"Error fetching instructor course details: {str(e)}")
1697
+ raise HTTPException(status_code=500, detail=str(e))
1698
+ finally:
1699
+ if 'conn' in locals():
1700
+ conn.close()
1701
+
1702
+ # CreateLecture model and create_lecture endpoint to handle lecture creation with video upload and quiz
1703
+ class CreateLecture(BaseModel):
1704
+ title: str
1705
+ description: str
1706
+ content: str
1707
+ quiz: Optional[dict] = None
1708
+
1709
+ @router.post("/courses/{course_id}/lectures")
1710
+ async def create_lecture(
1711
+ request: Request,
1712
+ course_id: int,
1713
+ auth_token: str = Cookie(None),
1714
+ title: str = Form(...),
1715
+ description: str = Form(...),
1716
+ content: str = Form(...),
1717
+ video: Optional[UploadFile] = File(None),
1718
+ quiz: Optional[str] = Form(None)
1719
+ ):
1720
+ try:
1721
+ # Get token from header if not in cookie
1722
+ if not auth_token:
1723
+ auth_header = request.headers.get('Authorization')
1724
+ if auth_header and auth_header.startswith('Bearer '):
1725
+ auth_token = auth_header.split(' ')[1]
1726
+ else:
1727
+ raise HTTPException(status_code=401, detail="No authentication token provided")
1728
+
1729
+ # Verify token and get user data
1730
+ try:
1731
+ user_data = decode_token(auth_token)
1732
+ username = user_data['username']
1733
+ role = user_data['role']
1734
+
1735
+ # Verify user is an instructor
1736
+ if role != "Instructor":
1737
+ raise HTTPException(status_code=403, detail="Only instructors can access this endpoint")
1738
+
1739
+ # Connect to database
1740
+ conn = connect_db()
1741
+ cursor = conn.cursor(pymysql.cursors.DictCursor)
1742
+
1743
+ try:
1744
+ # Get instructor ID
1745
+ cursor.execute("""
1746
+ SELECT InstructorID
1747
+ FROM Instructors
1748
+ WHERE AccountName = %s
1749
+ """, (username,))
1750
+
1751
+ instructor = cursor.fetchone()
1752
+ if not instructor:
1753
+ raise HTTPException(status_code=404, detail="Instructor not found")
1754
+
1755
+ instructor_id = instructor['InstructorID']
1756
+
1757
+ # Verify this instructor owns this course
1758
+ cursor.execute("""
1759
+ SELECT CourseID
1760
+ FROM Courses
1761
+ WHERE CourseID = %s AND InstructorID = %s
1762
+ """, (course_id, instructor_id))
1763
+
1764
+ if not cursor.fetchone():
1765
+ raise HTTPException(status_code=403, detail="Not authorized to modify this course")
1766
+
1767
+ # Create the lecture
1768
+ cursor.execute("""
1769
+ INSERT INTO Lectures (CourseID, Title, Description, Content)
1770
+ VALUES (%s, %s, %s, %s)
1771
+ """, (course_id, title, description, content))
1772
+ conn.commit()
1773
+
1774
+ # Get the newly created lecture ID
1775
+ lecture_id = cursor.lastrowid
1776
+
1777
+ # Upload video if provided
1778
+ if video:
1779
+ # Check file size (100MB limit)
1780
+ video_content = await video.read()
1781
+ if len(video_content) > 100 * 1024 * 1024: # 100MB in bytes
1782
+ raise HTTPException(status_code=400, detail="Video file size must be less than 100MB")
1783
+
1784
+ # Check file type
1785
+ if not video.content_type.startswith('video/'):
1786
+ raise HTTPException(status_code=400, detail="Invalid file type. Please upload a video file")
1787
+
1788
+ # Upload to S3
1789
+ media_path = f"videos/cid{course_id}/lid{lecture_id}/vid_lecture.mp4"
1790
+ s3.put_object(
1791
+ Bucket="tlhmaterials",
1792
+ Key=media_path,
1793
+ Body=video_content,
1794
+ ContentType=video.content_type,
1795
+ ACL="public-read",
1796
+ ContentDisposition="inline"
1797
+ )
1798
+
1799
+ # Create quiz if provided
1800
+ if quiz:
1801
+ quiz_data = json.loads(quiz)
1802
+ if quiz_data and quiz_data.get('questions'):
1803
+ # Insert quiz
1804
+ cursor.execute("""
1805
+ INSERT INTO Quizzes (LectureID, Title, Description)
1806
+ VALUES (%s, %s, %s)
1807
+ """, (lecture_id, f"Quiz for {title}", description))
1808
+ conn.commit()
1809
+
1810
+ quiz_id = cursor.lastrowid
1811
+
1812
+ # Insert questions and options
1813
+ for question in quiz_data['questions']:
1814
+ cursor.execute("""
1815
+ INSERT INTO Questions (QuizID, QuestionText)
1816
+ VALUES (%s, %s)
1817
+ """, (quiz_id, question['question']))
1818
+ conn.commit()
1819
+
1820
+ question_id = cursor.lastrowid
1821
+
1822
+ # Insert options
1823
+ for i, option in enumerate(question['options']):
1824
+ cursor.execute("""
1825
+ INSERT INTO Options (QuestionID, OptionText, IsCorrect)
1826
+ VALUES (%s, %s, %s)
1827
+ """, (question_id, option, i == question['correctAnswer']))
1828
+ conn.commit()
1829
+
1830
+ return {
1831
+ "id": lecture_id,
1832
+ "title": title,
1833
+ "message": "Lecture created successfully"
1834
+ }
1835
+
1836
+ except HTTPException:
1837
+ raise
1838
+ except Exception as e:
1839
+ print(f"Error in create_lecture: {str(e)}")
1840
+ raise HTTPException(status_code=500, detail=f"Failed to create lecture: {str(e)}")
1841
+
1842
+ except Exception as e:
1843
+ print(f"Token/user verification error: {str(e)}")
1844
+ raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
1845
+
1846
+ except HTTPException:
1847
+ raise
1848
+ except Exception as e:
1849
+ print(f"Error in create_lecture: {str(e)}")
1850
+ raise HTTPException(status_code=500, detail=str(e))
1851
+ finally:
1852
+ if 'conn' in locals():
1853
+ conn.close()
services/api/chat_endpoints.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, Request, Cookie
2
+ from typing import Dict
3
+ from services.api.db.token_utils import decode_token
4
+ from services.api.chatbot.core import get_chat_response, get_chat_response_lecture
5
+ from pydantic import BaseModel
6
+
7
+ router = APIRouter()
8
+
9
+ class ChatMessage(BaseModel):
10
+ message: str
11
+
12
+ @router.post("/chat")
13
+ async def chat_endpoint(message: ChatMessage, request: Request, auth_token: str = Cookie(None)):
14
+ try:
15
+ # Verify auth token
16
+ if not auth_token:
17
+ auth_header = request.headers.get('Authorization')
18
+ if auth_header and auth_header.startswith('Bearer '):
19
+ auth_token = auth_header.split(' ')[1]
20
+
21
+ if not auth_token:
22
+ raise HTTPException(status_code=401, detail="No authentication token provided")
23
+
24
+ try:
25
+ user_data = decode_token(auth_token)
26
+ except Exception as e:
27
+ print(f"Token decode error: {str(e)}")
28
+ raise HTTPException(status_code=401, detail="Invalid authentication token")
29
+
30
+ # Get chatbot response
31
+ response = get_chat_response(message.message)
32
+ return {"answer": response}
33
+
34
+ except HTTPException as he:
35
+ raise he
36
+ except Exception as e:
37
+ print(f"Chat error: {str(e)}")
38
+ raise HTTPException(status_code=500, detail=str(e))
39
+
40
+ @router.post("/chat/lecture/{lecture_id}")
41
+ async def lecture_chat_endpoint(
42
+ lecture_id: int,
43
+ message: ChatMessage,
44
+ request: Request,
45
+ auth_token: str = Cookie(None)
46
+ ):
47
+ try:
48
+ # Verify auth token
49
+ if not auth_token:
50
+ auth_header = request.headers.get('Authorization')
51
+ if auth_header and auth_header.startswith('Bearer '):
52
+ auth_token = auth_header.split(' ')[1]
53
+
54
+ if not auth_token:
55
+ raise HTTPException(status_code=401, detail="No authentication token provided")
56
+
57
+ try:
58
+ user_data = decode_token(auth_token)
59
+ except Exception as e:
60
+ print(f"Token decode error: {str(e)}")
61
+ raise HTTPException(status_code=401, detail="Invalid authentication token")
62
+
63
+ # Get chatbot response for specific lecture
64
+ response = get_chat_response_lecture(message.message, lecture_id)
65
+ return {"answer": response}
66
+
67
+ except HTTPException as he:
68
+ raise he
69
+ except Exception as e:
70
+ print(f"Lecture chat error: {str(e)}")
71
+ raise HTTPException(status_code=500, detail=str(e))
72
+
73
+ @router.delete("/chat/history")
74
+ async def clear_history_endpoint(request: Request, auth_token: str = Cookie(None)):
75
+ try:
76
+ # Verify auth token
77
+ if not auth_token:
78
+ auth_header = request.headers.get('Authorization')
79
+ if auth_header and auth_header.startswith('Bearer '):
80
+ auth_token = auth_header.split(' ')[1]
81
+
82
+ if not auth_token:
83
+ raise HTTPException(status_code=401, detail="No authentication token provided")
84
+
85
+ try:
86
+ user_data = decode_token(auth_token)
87
+ except Exception as e:
88
+ print(f"Token decode error: {str(e)}")
89
+ raise HTTPException(status_code=401, detail="Invalid authentication token")
90
+
91
+ # Clear both regular and lecture chat history
92
+ from services.api.chatbot.core import clear_chat_history, clear_lecture_chat_history
93
+ clear_chat_history()
94
+ clear_lecture_chat_history()
95
+
96
+ return {"status": "success", "message": "Chat history cleared"}
97
+
98
+ except HTTPException as he:
99
+ raise he
100
+ except Exception as e:
101
+ print(f"Clear history error: {str(e)}")
102
+ raise HTTPException(status_code=500, detail=str(e))
services/api/chatbot/__pycache__/config.cpython-312.pyc ADDED
Binary file (958 Bytes). View file
 
services/api/chatbot/__pycache__/config.cpython-313.pyc ADDED
Binary file (956 Bytes). View file
 
services/api/chatbot/__pycache__/core.cpython-312.pyc ADDED
Binary file (4.91 kB). View file
 
services/api/chatbot/__pycache__/core.cpython-313.pyc ADDED
Binary file (4.25 kB). View file
 
services/api/chatbot/__pycache__/llm.cpython-312.pyc ADDED
Binary file (1.98 kB). View file
 
services/api/chatbot/__pycache__/llm.cpython-313.pyc ADDED
Binary file (2.06 kB). View file
 
services/api/chatbot/__pycache__/prompts.cpython-312.pyc ADDED
Binary file (2.68 kB). View file
 
services/api/chatbot/__pycache__/prompts.cpython-313.pyc ADDED
Binary file (2.5 kB). View file
 
services/api/chatbot/__pycache__/retrieval.cpython-312.pyc ADDED
Binary file (23.7 kB). View file
 
services/api/chatbot/__pycache__/retrieval.cpython-313.pyc ADDED
Binary file (15.1 kB). View file
 
services/api/chatbot/config.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import google.generativeai as genai
2
+ from langchain.llms.base import LLM
3
+ from typing import List, Optional
4
+ from dotenv import load_dotenv
5
+ import os
6
+
7
+ load_dotenv()
8
+
9
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
10
+ QDRANT_HOST = os.getenv("QDRANT_HOST")
11
+ QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
12
+
13
+ QDRANT_COLLECTION_NAME = "dbms_collection_courses"
14
+ QDRANT_COLLECTION_NAME_LECTURES = "dbms_collection_lectures"
15
+ MODEL_NAME = "gemini-2.0-flash"
16
+
17
+ EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
18
+ EMBEDDING_SIZE = 384
services/api/chatbot/core.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .llm import gemini_llm
2
+ from langchain.chains import ConversationalRetrievalChain
3
+ from langchain.memory import ConversationBufferMemory
4
+ from .retrieval import get_vectorstore, sync_courses_to_qdrant, get_vectorstore_lectures
5
+ from langchain.chains import RetrievalQA
6
+ from .prompts import courses_prompt, condense_prompt, lectures_prompt
7
+ from qdrant_client.http import models
8
+
9
+
10
+ import qdrant_client
11
+ from .config import (
12
+ EMBEDDING_MODEL,
13
+ QDRANT_HOST,
14
+ QDRANT_API_KEY,
15
+ QDRANT_COLLECTION_NAME,
16
+ EMBEDDING_SIZE,
17
+ QDRANT_COLLECTION_NAME_LECTURES,
18
+ )
19
+ # from langchain.retrievers.self_query.base import SelfQueryRetriever
20
+ # from langchain.retrievers.self_query.qdrant import QdrantTranslator
21
+ client = qdrant_client.QdrantClient(QDRANT_HOST, api_key=QDRANT_API_KEY)
22
+
23
+ vector_store = get_vectorstore()
24
+ vector_store_lecture = get_vectorstore_lectures()
25
+ memory = ConversationBufferMemory(
26
+ memory_key="chat_history",
27
+ return_messages=True,
28
+ output_key="answer"
29
+ )
30
+
31
+ memory_lecture = ConversationBufferMemory(
32
+ memory_key="chat_history",
33
+ return_messages=True,
34
+ output_key="answer"
35
+ )
36
+
37
+ qa_chain = ConversationalRetrievalChain.from_llm(
38
+ llm=gemini_llm,
39
+ retriever=vector_store.as_retriever(search_kwargs={"k": 5}),
40
+ memory=memory,
41
+ condense_question_prompt=condense_prompt,
42
+ combine_docs_chain_kwargs={
43
+ "prompt": courses_prompt
44
+ },
45
+ return_source_documents=True
46
+ )
47
+
48
+ def bulid_qa_chain(lecture_id: int):
49
+ return ConversationalRetrievalChain.from_llm(
50
+ llm=gemini_llm,
51
+ retriever = vector_store_lecture.as_retriever(
52
+ search_kwargs={
53
+ "k": 5,
54
+ "filter": models.Filter(
55
+ must=[
56
+ models.FieldCondition(
57
+ key="metadata.LectureID",
58
+ match=models.MatchValue(value=lecture_id),
59
+ )
60
+ ]
61
+ )
62
+ }
63
+ ),
64
+ memory=memory_lecture,
65
+ condense_question_prompt=condense_prompt,
66
+ combine_docs_chain_kwargs={
67
+ "prompt": lectures_prompt
68
+ },
69
+ return_source_documents=True
70
+ )
71
+
72
+ def get_chat_response_lecture(user_input: str, lecture_id: int) -> str:
73
+ print(f"\n[DEBUG] Sample points from Qdrant for LectureID={lecture_id}")
74
+ points, _ = client.scroll(
75
+ collection_name=QDRANT_COLLECTION_NAME_LECTURES,
76
+ limit=10,
77
+ with_payload=True,
78
+ with_vectors=False,
79
+ scroll_filter=models.Filter(
80
+ must=[
81
+ models.FieldCondition(
82
+ key="metadata.LectureID",
83
+ match=models.MatchValue(value=lecture_id)
84
+ )
85
+ ]
86
+ )
87
+ )
88
+ print(points[0].payload)
89
+ qa_chain_lecture = bulid_qa_chain(lecture_id)
90
+ response = qa_chain_lecture({"question": user_input})
91
+ print(lecture_id)
92
+ print()
93
+ print("Source Documents:")
94
+ for i, doc in enumerate(response["source_documents"], start=1):
95
+ cid = doc.metadata.get("LectureID", "N/A")
96
+ content = doc.page_content
97
+ print(f"{i}. CourseID={cid}\n {content}\n")
98
+ return response["answer"]
99
+
100
+ def get_chat_response(user_input: str) -> str:
101
+ response = qa_chain({"question": user_input})
102
+ print("Source Documents:")
103
+ for i, doc in enumerate(response["source_documents"], start=1):
104
+ cid = doc.metadata.get("CourseID", "N/A")
105
+ content = doc.page_content
106
+ print(f"{i}. CourseID={cid}\n {content}\n")
107
+ return response["answer"]
108
+
109
+ def clear_chat_history():
110
+ """Clear the conversation memory for general chat"""
111
+ memory.clear()
112
+ return {"status": "success", "message": "Chat history cleared"}
113
+
114
+ def clear_lecture_chat_history():
115
+ """Clear the conversation memory for lecture-specific chat"""
116
+ memory_lecture.clear()
117
+ return {"status": "success", "message": "Lecture chat history cleared"}
services/api/chatbot/llm.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import google.generativeai as genai
2
+ from langchain.llms.base import LLM
3
+ from typing import Optional, List
4
+ from .config import GEMINI_API_KEY, MODEL_NAME
5
+
6
+ genai.configure(api_key=GEMINI_API_KEY)
7
+
8
+ class GeminiWrapper(LLM):
9
+ """Wrapper để sử dụng Gemini với LangChain."""
10
+
11
+ model: str = MODEL_NAME
12
+
13
+ def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str:
14
+ """Gửi prompt đến Gemini và trả về kết quả."""
15
+ model = genai.GenerativeModel(self.model)
16
+ response = model.generate_content(prompt)
17
+ return response.text if response and hasattr(response, 'text') else "Không có phản hồi từ Gemini."
18
+
19
+ @property
20
+ def _identifying_params(self) -> dict:
21
+ """Trả về tham số nhận diện của mô hình."""
22
+ return {"model": self.model}
23
+
24
+ @property
25
+ def _llm_type(self) -> str:
26
+ return "gemini"
27
+
28
+ gemini_llm = GeminiWrapper()
29
+
services/api/chatbot/memory.py ADDED
File without changes
services/api/chatbot/prompts.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain.prompts import PromptTemplate
2
+ # from .llm import gemini_llm
3
+ # from langchain_core.prompts import ChatPromptTemplate
4
+
5
+ courses_prompt = PromptTemplate(
6
+ input_variables=["context", "question"],
7
+ template="""
8
+ Your name is Edumate. You are an AI assistant of THE LEARNING HOUSE — an online learning platform that provides a wide variety of courses.
9
+
10
+ Your mission is to help users find courses that match their needs and answer any additional questions they might have.
11
+
12
+ If users didn't show their demand to learning(e.g: They gretting, introduce, ...) introduce about yourself, that you're here to help them find most suitable course.
13
+ If enough information that show their preference, lets priotize recommend user with suitable course
14
+
15
+ Below is a list of courses retrieved from our database. Recommend relevant courses to the user by providing helpful information about each course.
16
+ {context}
17
+ If none of the available courses match the user's demand, politely inform them that we currently do not have a suitable course and that their request has been noted for future development.
18
+
19
+ This is the user's question:
20
+ {question}
21
+
22
+ You must answer in the same language as users.
23
+ ---
24
+
25
+ ### Response:
26
+ """
27
+ )
28
+
29
+
30
+ lectures_prompt = PromptTemplate(
31
+ input_variables=["context", "question"],
32
+ template="""
33
+ You are an AI Teaching Assistant at THE LEARNING HOUSE. Your mission is to help learners better understand and engage with the course content.
34
+
35
+ Below is a passage from the course the learner is currently studying. You are an expert in this field, and your role is to support the learner by doing any of the following:
36
+ - Explain difficult parts in a simpler way
37
+ - Provide relevant examples or analogies
38
+ - Share fun or interesting facts
39
+ - Ask questions to reinforce understanding
40
+ - Summarize the content if needed
41
+
42
+ Course content:
43
+ {context}
44
+
45
+ Now, here is the learner's question:
46
+ {question}
47
+
48
+ If none of the available content seems related to the learner's question, kindly inform them that we currently do not have suitable material for this request and that their feedback has been noted for future development.
49
+
50
+ You must answer in the same language as users.
51
+ ---
52
+
53
+ ### Response:
54
+ """
55
+ )
56
+
57
+
58
+ condense_prompt = PromptTemplate(
59
+ input_variables=["question", "chat_history"],
60
+ template="""
61
+ Given the following conversation and a follow-up question, rephrase the follow-up question to be a standalone question.
62
+
63
+ Chat History:
64
+ {chat_history}
65
+
66
+ Follow-Up Input:
67
+ {question}
68
+
69
+ Standalone question:
70
+
71
+ You must answer in the same language as users.
72
+ """
73
+ )
services/api/chatbot/retrieval.py ADDED
@@ -0,0 +1,531 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import hashlib
4
+ import pymysql
5
+ import qdrant_client
6
+ import asyncio
7
+ import aiomysql
8
+ from dotenv import load_dotenv
9
+ from langchain.schema import Document
10
+ from langchain.vectorstores import Qdrant
11
+ from langchain_huggingface import HuggingFaceEmbeddings
12
+ from qdrant_client.http.models import PointIdsList, Distance, VectorParams
13
+ from typing import List, Tuple, Dict, Set
14
+ from .config import (
15
+ EMBEDDING_MODEL,
16
+ QDRANT_HOST,
17
+ QDRANT_API_KEY,
18
+ QDRANT_COLLECTION_NAME,
19
+ EMBEDDING_SIZE,
20
+ QDRANT_COLLECTION_NAME_LECTURES,
21
+ )
22
+
23
+ load_dotenv()
24
+
25
+ #--- MySQL connection ---
26
+ MYSQL_USER = os.getenv("MYSQL_USER")
27
+ MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD")
28
+ MYSQL_HOST = os.getenv("MYSQL_HOST")
29
+ MYSQL_DB = os.getenv("MYSQL_DB")
30
+ MYSQL_PORT = int(os.getenv("MYSQL_PORT", 3306))
31
+
32
+ # Initialize embedding model with a more compatible configuration
33
+ embedding_model = HuggingFaceEmbeddings(
34
+ model_name="sentence-transformers/all-MiniLM-L6-v2",
35
+ model_kwargs={'device': 'cpu'},
36
+ encode_kwargs={'normalize_embeddings': True}
37
+ )
38
+ client = qdrant_client.QdrantClient(QDRANT_HOST, api_key=QDRANT_API_KEY)
39
+
40
+ def connect_db():
41
+ return pymysql.connect(
42
+ host=MYSQL_HOST,
43
+ user=MYSQL_USER,
44
+ password=MYSQL_PASSWORD,
45
+ database=MYSQL_DB,
46
+ port=MYSQL_PORT,
47
+ cursorclass=pymysql.cursors.DictCursor,
48
+ )
49
+
50
+ async def connect_db_async():
51
+ return await aiomysql.connect(
52
+ host=MYSQL_HOST,
53
+ user=MYSQL_USER,
54
+ password=MYSQL_PASSWORD,
55
+ database=MYSQL_DB,
56
+ port=MYSQL_PORT,
57
+ cursorclass=aiomysql.DictCursor,
58
+ )
59
+
60
+ #---- Lectures processing
61
+
62
+ def hash_lectures(l: dict) ->str:
63
+ text = "|".join([
64
+ str(l.get("CourseName", "")),
65
+ str(l.get("Descriptions", "")),
66
+ str(l.get("Skills", "")),
67
+ str(l.get("EstimatedDuration", "")),
68
+ str(l.get("Difficulty", "")),
69
+ str(l.get("AverageRating", "")),
70
+ ])
71
+ return hashlib.md5(text.encode("utf-8")).hexdigest()
72
+
73
+ def load_sql_lectures(): # -> list[dict]
74
+ with connect_db() as conn:
75
+ with conn.cursor() as cursor:
76
+ cursor.execute("""
77
+ SELECT LectureID, Title, Description, Content
78
+ FROM Lectures
79
+ """)
80
+ return cursor.fetchall()
81
+
82
+ async def load_sql_lectures_async() -> List[Dict]:
83
+ async with await connect_db_async() as conn:
84
+ async with conn.cursor() as cursor:
85
+ await cursor.execute("""
86
+ SELECT LectureID, Title, Description, Content
87
+ FROM Lectures
88
+ """)
89
+ return await cursor.fetchall()
90
+
91
+ def convert_to_documents_lectures(lectures: list[dict]) -> list[Document]:
92
+ documents: list[Document] = []
93
+ for l in lectures:
94
+ parts = [
95
+ f"Lecture Title: {l.get('Title', 'No title')}",
96
+ f"Description: {l.get('Description', 'No description')}",
97
+ f"Content: {l.get('Content', 'None')}",
98
+ ]
99
+ text = ", ".join(parts)
100
+ text = re.sub(r"\s+", " ", text).strip()
101
+
102
+ metadata = {
103
+ "LectureID": l["LectureID"],
104
+ "hash": hash_lectures(l)
105
+ }
106
+
107
+ documents.append(Document(page_content=text, metadata=metadata))
108
+ return documents
109
+
110
+ def get_existing_qdrant_data_lectures() -> tuple[set[int], dict[int,str]]:
111
+ qdrant_ids: set[int] = set()
112
+ qdrant_hash_map: dict[int,str] = {}
113
+
114
+ scroll_offset = None
115
+ while True:
116
+ points, scroll_offset = client.scroll(
117
+ collection_name=QDRANT_COLLECTION_NAME_LECTURES,
118
+ limit=1000,
119
+ with_payload=True,
120
+ offset=scroll_offset,
121
+ )
122
+ for pt in points:
123
+ cid = pt.payload["metadata"]["LectureID"]
124
+ qdrant_ids.add(cid)
125
+ qdrant_hash_map[cid] = pt.payload["metadata"].get("hash", "")
126
+ if scroll_offset is None:
127
+ break
128
+
129
+ return qdrant_ids, qdrant_hash_map
130
+
131
+ async def get_existing_qdrant_data_lectures_async() -> Tuple[Set[int], Dict[int, str]]:
132
+ qdrant_ids: set[int] = set()
133
+ qdrant_hash_map: dict[int,str] = {}
134
+
135
+ scroll_offset = None
136
+ while True:
137
+ # Note: client.scroll is synchronous but we're keeping the async pattern
138
+ points, scroll_offset = client.scroll(
139
+ collection_name=QDRANT_COLLECTION_NAME_LECTURES,
140
+ limit=1000,
141
+ with_payload=True,
142
+ offset=scroll_offset,
143
+ )
144
+ for pt in points:
145
+ cid = pt.payload["metadata"]["LectureID"]
146
+ qdrant_ids.add(cid)
147
+ qdrant_hash_map[cid] = pt.payload["metadata"].get("hash", "")
148
+ if scroll_offset is None:
149
+ break
150
+
151
+ return qdrant_ids, qdrant_hash_map
152
+
153
+ def sync_lectures_to_qdrant():
154
+ cols = client.get_collections().collections
155
+ if not any(c.name == QDRANT_COLLECTION_NAME_LECTURES for c in cols):
156
+ client.create_collection(
157
+ collection_name=QDRANT_COLLECTION_NAME_LECTURES,
158
+ vectors_config=VectorParams(size=EMBEDDING_SIZE, distance=Distance.COSINE),
159
+ )
160
+
161
+ # 2) Load data from MySQL
162
+ db_lectures = load_sql_lectures()
163
+ db_map = {l["LectureID"]: l for l in db_lectures}
164
+ db_ids = set(db_map.keys())
165
+
166
+ # 3) Load data from Qdrant
167
+ qdrant_ids, qdrant_hash_map = get_existing_qdrant_data_lectures()
168
+
169
+ # 4) detemine new / removed / updated
170
+ new_ids = db_ids - qdrant_ids
171
+ removed_ids = qdrant_ids - db_ids
172
+ updated_ids = {
173
+ cid
174
+ for cid in db_ids & qdrant_ids
175
+ if hash_lectures(db_map[cid]) != qdrant_hash_map.get(cid, "")
176
+ }
177
+
178
+ # 5) Upsert & update
179
+ to_upsert = [db_map[cid] for cid in new_ids | updated_ids]
180
+ if to_upsert:
181
+ docs = convert_to_documents_lectures(to_upsert)
182
+ vs = Qdrant(
183
+ client=client,
184
+ collection_name=QDRANT_COLLECTION_NAME_LECTURES,
185
+ embeddings=embedding_model,
186
+ content_payload_key="page_content",
187
+ metadata_payload_key="metadata",
188
+ )
189
+ vs.add_documents(docs)
190
+ print(f"Added/Updated: {len(docs)} documents.")
191
+
192
+ # 6) Delete Unavailable courses
193
+ if removed_ids:
194
+ client.delete(
195
+ collection_name=QDRANT_COLLECTION_NAME_LECTURES,
196
+ points_selector=PointIdsList(points=list(removed_ids)),
197
+ )
198
+ print(f"Removed: {len(removed_ids)} documents.")
199
+
200
+ print(
201
+ f"Sync completed. "
202
+ f"New: {len(new_ids)}, "
203
+ f"Updated: {len(updated_ids)}, "
204
+ f"Removed: {len(removed_ids)}"
205
+ )
206
+ collection_info = client.get_collection(QDRANT_COLLECTION_NAME_LECTURES)
207
+ total_points = collection_info.points_count
208
+ print(f"Number of vector in Vectordb: {total_points}")
209
+
210
+ async def sync_lectures_to_qdrant_async():
211
+ cols = client.get_collections().collections
212
+ if not any(c.name == QDRANT_COLLECTION_NAME_LECTURES for c in cols):
213
+ client.create_collection(
214
+ collection_name=QDRANT_COLLECTION_NAME_LECTURES,
215
+ vectors_config=VectorParams(size=EMBEDDING_SIZE, distance=Distance.COSINE),
216
+ )
217
+
218
+ # 2) Load data from MySQL
219
+ db_lectures = await load_sql_lectures_async()
220
+ db_map = {l["LectureID"]: l for l in db_lectures}
221
+ db_ids = set(db_map.keys())
222
+
223
+ # 3) Load data from Qdrant
224
+ qdrant_ids, qdrant_hash_map = await get_existing_qdrant_data_lectures_async()
225
+
226
+ # 4) determine new / removed / updated
227
+ new_ids = db_ids - qdrant_ids
228
+ removed_ids = qdrant_ids - db_ids
229
+ updated_ids = {
230
+ cid
231
+ for cid in db_ids & qdrant_ids
232
+ if hash_lectures(db_map[cid]) != qdrant_hash_map.get(cid, "")
233
+ }
234
+
235
+ # 5) Upsert & update
236
+ to_upsert = [db_map[cid] for cid in new_ids | updated_ids]
237
+ if to_upsert:
238
+ docs = convert_to_documents_lectures(to_upsert)
239
+ vs = Qdrant(
240
+ client=client,
241
+ collection_name=QDRANT_COLLECTION_NAME_LECTURES,
242
+ embeddings=embedding_model,
243
+ content_payload_key="page_content",
244
+ metadata_payload_key="metadata",
245
+ )
246
+ await asyncio.to_thread(vs.add_documents, docs)
247
+ print(f"Added/Updated: {len(docs)} documents.")
248
+
249
+ # 6) Delete Unavailable courses
250
+ if removed_ids:
251
+ await asyncio.to_thread(
252
+ client.delete,
253
+ collection_name=QDRANT_COLLECTION_NAME_LECTURES,
254
+ points_selector=PointIdsList(points=list(removed_ids)),
255
+ )
256
+ print(f"Removed: {len(removed_ids)} documents.")
257
+
258
+ print(
259
+ f"Sync completed. "
260
+ f"New: {len(new_ids)}, "
261
+ f"Updated: {len(updated_ids)}, "
262
+ f"Removed: {len(removed_ids)}"
263
+ )
264
+ collection_info = client.get_collection(QDRANT_COLLECTION_NAME_LECTURES)
265
+ total_points = collection_info.points_count
266
+ print(f"Number of vectors in Vectordb: {total_points}")
267
+
268
+ def get_vectorstore_lectures() -> Qdrant:
269
+ return Qdrant(
270
+ client=client,
271
+ collection_name=QDRANT_COLLECTION_NAME_LECTURES,
272
+ embeddings=embedding_model,
273
+ content_payload_key="page_content",
274
+ metadata_payload_key="metadata",
275
+ )
276
+
277
+ def get_vectorstore() -> Qdrant:
278
+ """Alias for get_vectorstore_lectures for backward compatibility"""
279
+ return get_vectorstore_lectures()
280
+
281
+ async def reset_qdrant_collection_async():
282
+ collections = client.get_collections().collections
283
+ if any(c.name == QDRANT_COLLECTION_NAME_LECTURES for c in collections):
284
+ await asyncio.to_thread(client.delete_collection, QDRANT_COLLECTION_NAME_LECTURES)
285
+ print(f"Đã xoá collection: {QDRANT_COLLECTION_NAME_LECTURES}")
286
+
287
+ await asyncio.to_thread(
288
+ client.create_collection,
289
+ collection_name=QDRANT_COLLECTION_NAME_LECTURES,
290
+ vectors_config=VectorParams(size=EMBEDDING_SIZE, distance=Distance.COSINE),
291
+ )
292
+ print(f"Đã khởi tạo lại collection: {QDRANT_COLLECTION_NAME_LECTURES}")
293
+
294
+
295
+
296
+
297
+
298
+
299
+
300
+
301
+
302
+
303
+
304
+
305
+
306
+
307
+
308
+
309
+
310
+
311
+
312
+
313
+ #---- Courses processing
314
+ def hash_course(c: dict) -> str:
315
+ text = "|".join([
316
+ str(c.get("CourseName", "")),
317
+ str(c.get("Descriptions", "")),
318
+ str(c.get("Skills", "")),
319
+ str(c.get("EstimatedDuration", "")),
320
+ str(c.get("Difficulty", "")),
321
+ str(c.get("AverageRating", "")),
322
+ ])
323
+ return hashlib.md5(text.encode("utf-8")).hexdigest()
324
+
325
+ def load_sql(): #-> list[dict]
326
+ with connect_db() as conn:
327
+ with conn.cursor() as cursor:
328
+ cursor.execute("SELECT * FROM Courses")
329
+ return cursor.fetchall()
330
+
331
+ async def load_sql_async() -> List[Dict]:
332
+ async with await connect_db_async() as conn:
333
+ async with conn.cursor() as cursor:
334
+ await cursor.execute("SELECT * FROM Courses")
335
+ return await cursor.fetchall()
336
+
337
+ def convert_to_documents(courses: list[dict]) -> list[Document]:
338
+ documents: list[Document] = []
339
+ for c in courses:
340
+ # 1) Build the textual content
341
+ parts = [
342
+ f"CourseName: {c.get('CourseName', 'No title')}",
343
+ f"Descriptions: {c.get('Descriptions', 'No description')}",
344
+ f"Skills: {c.get('Skills', 'None')}",
345
+ f"EstimatedDuration (hours): {c.get('EstimatedDuration', 'Unknown')}",
346
+ f"Difficulty: {c.get('Difficulty', 'Unknown')}",
347
+ f"AverageRating: {c.get('AverageRating', '0.00')}",
348
+ ]
349
+ text = ", ".join(parts)
350
+ text = re.sub(r"\s+", " ", text).strip()
351
+
352
+ # 2) Assemble metadata
353
+ metadata = {
354
+ "CourseID": c["CourseID"],
355
+ "Skills": c.get("Skills", ""),
356
+ "EstimatedDuration": c.get("EstimatedDuration", 0),
357
+ "Difficulty": c.get("Difficulty", ""),
358
+ "AverageRating": float(c.get("AverageRating", 0.0)),
359
+ "hash": hash_course(c),
360
+ }
361
+
362
+ documents.append(Document(page_content=text, metadata=metadata))
363
+ return documents
364
+
365
+
366
+ def get_existing_qdrant_data() -> tuple[set[int], dict[int,str]]:
367
+ qdrant_ids: set[int] = set()
368
+ qdrant_hash_map: dict[int,str] = {}
369
+
370
+ scroll_offset = None
371
+ while True:
372
+ points, scroll_offset = client.scroll(
373
+ collection_name=QDRANT_COLLECTION_NAME,
374
+ limit=1000,
375
+ with_payload=True,
376
+ offset=scroll_offset,
377
+ )
378
+ for pt in points:
379
+ cid = pt.payload["metadata"]["CourseID"]
380
+ qdrant_ids.add(cid)
381
+ qdrant_hash_map[cid] = pt.payload["metadata"].get("hash", "")
382
+ if scroll_offset is None:
383
+ break
384
+
385
+ return qdrant_ids, qdrant_hash_map
386
+
387
+ async def get_existing_qdrant_data_async() -> Tuple[Set[int], Dict[int, str]]:
388
+ qdrant_ids: set[int] = set()
389
+ qdrant_hash_map: dict[int,str] = {}
390
+
391
+ scroll_offset = None
392
+ while True:
393
+ points, scroll_offset = client.scroll(
394
+ collection_name=QDRANT_COLLECTION_NAME,
395
+ limit=1000,
396
+ with_payload=True,
397
+ offset=scroll_offset,
398
+ )
399
+ for pt in points:
400
+ cid = pt.payload["metadata"]["CourseID"]
401
+ qdrant_ids.add(cid)
402
+ qdrant_hash_map[cid] = pt.payload["metadata"].get("hash", "")
403
+ if scroll_offset is None:
404
+ break
405
+
406
+ return qdrant_ids, qdrant_hash_map
407
+
408
+ def sync_courses_to_qdrant():
409
+ cols = client.get_collections().collections
410
+ if not any(c.name == QDRANT_COLLECTION_NAME for c in cols):
411
+ client.create_collection(
412
+ collection_name=QDRANT_COLLECTION_NAME,
413
+ vectors_config=VectorParams(size=EMBEDDING_SIZE, distance=Distance.COSINE),
414
+ )
415
+
416
+ # 2) Load data from MySQL
417
+ db_courses = load_sql()
418
+ db_map = {c["CourseID"]: c for c in db_courses}
419
+ db_ids = set(db_map.keys())
420
+
421
+ # 3) Load data from Qdrant
422
+ qdrant_ids, qdrant_hash_map = get_existing_qdrant_data()
423
+
424
+ # 4) detemine new / removed / updated
425
+ new_ids = db_ids - qdrant_ids
426
+ removed_ids = qdrant_ids - db_ids
427
+ updated_ids = {
428
+ cid
429
+ for cid in db_ids & qdrant_ids
430
+ if hash_course(db_map[cid]) != qdrant_hash_map.get(cid, "")
431
+ }
432
+
433
+ # 5) Upsert & update
434
+ to_upsert = [db_map[cid] for cid in new_ids | updated_ids]
435
+ if to_upsert:
436
+ docs = convert_to_documents(to_upsert)
437
+ vs = Qdrant(
438
+ client=client,
439
+ collection_name=QDRANT_COLLECTION_NAME,
440
+ embeddings=embedding_model,
441
+ content_payload_key="page_content",
442
+ metadata_payload_key="metadata",
443
+ )
444
+ vs.add_documents(docs)
445
+ print(f"Added/Updated: {len(docs)} documents.")
446
+
447
+ # 6) Delete Unavailable courses
448
+ if removed_ids:
449
+ client.delete(
450
+ collection_name=QDRANT_COLLECTION_NAME,
451
+ points_selector=PointIdsList(points=list(removed_ids)),
452
+ )
453
+ print(f"🗑 Removed: {len(removed_ids)} documents.")
454
+
455
+ print(
456
+ f"Sync completed. "
457
+ f"New: {len(new_ids)}, "
458
+ f"Updated: {len(updated_ids)}, "
459
+ f"Removed: {len(removed_ids)}"
460
+ )
461
+ collection_info = client.get_collection(QDRANT_COLLECTION_NAME)
462
+ total_points = collection_info.points_count
463
+ print(f"Number of vector in Vectordb: {total_points}")
464
+
465
+ async def sync_courses_to_qdrant_async():
466
+ cols = client.get_collections().collections
467
+ if not any(c.name == QDRANT_COLLECTION_NAME for c in cols):
468
+ client.create_collection(
469
+ collection_name=QDRANT_COLLECTION_NAME,
470
+ vectors_config=VectorParams(size=EMBEDDING_SIZE, distance=Distance.COSINE),
471
+ )
472
+
473
+ # 2) Load data from MySQL
474
+ db_courses = await load_sql_async()
475
+ db_map = {c["CourseID"]: c for c in db_courses}
476
+ db_ids = set(db_map.keys())
477
+
478
+ # 3) Load data from Qdrant
479
+ qdrant_ids, qdrant_hash_map = await get_existing_qdrant_data_async()
480
+
481
+ # 4) determine new / removed / updated
482
+ new_ids = db_ids - qdrant_ids
483
+ removed_ids = qdrant_ids - db_ids
484
+ updated_ids = {
485
+ cid
486
+ for cid in db_ids & qdrant_ids
487
+ if hash_course(db_map[cid]) != qdrant_hash_map.get(cid, "")
488
+ }
489
+
490
+ # 5) Upsert & update
491
+ to_upsert = [db_map[cid] for cid in new_ids | updated_ids]
492
+ if to_upsert:
493
+ docs = convert_to_documents(to_upsert)
494
+ vs = Qdrant(
495
+ client=client,
496
+ collection_name=QDRANT_COLLECTION_NAME,
497
+ embeddings=embedding_model,
498
+ content_payload_key="page_content",
499
+ metadata_payload_key="metadata",
500
+ )
501
+ await asyncio.to_thread(vs.add_documents, docs)
502
+ print(f"Added/Updated: {len(docs)} documents.")
503
+
504
+ # 6) Delete Unavailable courses
505
+ if removed_ids:
506
+ await asyncio.to_thread(
507
+ client.delete,
508
+ collection_name=QDRANT_COLLECTION_NAME,
509
+ points_selector=PointIdsList(points=list(removed_ids)),
510
+ )
511
+ print(f"🗑 Removed: {len(removed_ids)} documents.")
512
+
513
+ print(
514
+ f"Sync completed. "
515
+ f"New: {len(new_ids)}, "
516
+ f"Updated: {len(updated_ids)}, "
517
+ f"Removed: {len(removed_ids)}"
518
+ )
519
+
520
+
521
+ def reset_qdrant_collection():
522
+ collections = client.get_collections().collections
523
+ if any(c.name == QDRANT_COLLECTION_NAME for c in collections):
524
+ client.delete_collection(QDRANT_COLLECTION_NAME)
525
+ print(f"Đã xoá collection: {QDRANT_COLLECTION_NAME}")
526
+
527
+ client.create_collection(
528
+ collection_name=QDRANT_COLLECTION_NAME,
529
+ vectors_config=VectorParams(size=EMBEDDING_SIZE, distance=Distance.COSINE),
530
+ )
531
+ print(f"Đã khởi tạo lại collection: {QDRANT_COLLECTION_NAME}")
services/api/chatbot/self_query.py ADDED
File without changes
services/api/db/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (231 Bytes). View file
 
services/api/db/__pycache__/auth.cpython-312.pyc ADDED
Binary file (20.5 kB). View file
 
services/api/db/__pycache__/db_connection.cpython-312.pyc ADDED
Binary file (1.5 kB). View file
 
services/api/db/__pycache__/token_utils.cpython-312.pyc ADDED
Binary file (2.24 kB). View file
 
services/api/db/auth.py ADDED
@@ -0,0 +1,437 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ auth.py – Lightweight authentication micro-service
3
+ ----------------------------------------------------
4
+ • Exposes /auth/verify_user (login check)
5
+ • Optional /auth/register_user for sign-up
6
+ • Uses MySQL, bcrypt, and Pydantic
7
+ • No Streamlit, cookies, or front-end logic
8
+ """
9
+
10
+ from fastapi import FastAPI, Request, Response, Cookie, HTTPException, Depends
11
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
12
+ from fastapi.responses import HTMLResponse, JSONResponse
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from fastapi.security import OAuth2PasswordRequestForm
15
+ from jose import jwt, JWTError
16
+ from pydantic import BaseModel
17
+ from dotenv import load_dotenv
18
+ import os
19
+ import time
20
+ import requests
21
+ import os
22
+ import pymysql
23
+ import bcrypt
24
+ import sys
25
+ import importlib.util
26
+ from services.api.db.token_utils import create_token, decode_token
27
+ from sqlalchemy.orm import Session
28
+
29
+ # Add parent directory to path to enable imports
30
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
31
+
32
+ # Create FastAPI app
33
+ app = FastAPI()
34
+
35
+ # Configure CORS
36
+ app.add_middleware(
37
+ CORSMiddleware,
38
+ allow_origins=["http://localhost:3000, https://tlong-ds.github.io/thelearninghouse/"], # Your React frontend URL
39
+ allow_credentials=True, # Important for cookies
40
+ allow_methods=["*"],
41
+ allow_headers=["*"],
42
+ expose_headers=["*"]
43
+ )
44
+
45
+ # Import API endpoints
46
+ try:
47
+ from services.api.api_endpoints import router as api_router
48
+ except ImportError:
49
+ # Try a different import strategy for direct module execution
50
+ spec = importlib.util.spec_from_file_location(
51
+ "api_endpoints",
52
+ os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "api_endpoints.py")
53
+ )
54
+ api_endpoints = importlib.util.module_from_spec(spec)
55
+ spec.loader.exec_module(api_endpoints)
56
+ api_router = api_endpoints.router
57
+
58
+
59
+ # Load environment variables
60
+ load_dotenv()
61
+ SECRET_KEY = os.getenv("SECRET_TOKEN", "dev-secret")
62
+ ALGORITHM = os.getenv("ALGORITHM", "HS256")
63
+ MYSQL_USER = os.getenv("MYSQL_USER")
64
+ MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD")
65
+ MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost")
66
+ MYSQL_DB = os.getenv("MYSQL_DB")
67
+ MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
68
+
69
+
70
+
71
+ # Initialize FastAPI app
72
+ app = FastAPI()
73
+ app.add_middleware(
74
+ CORSMiddleware,
75
+ allow_origins=["http://localhost:8503", "http://localhost:3000", "http://127.0.0.1:3000", "https://tlong-ds.github.io"],
76
+ allow_credentials=True,
77
+ allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
78
+ allow_headers=["*"],
79
+ expose_headers=["*"],
80
+ )
81
+
82
+ # Import and include chat router
83
+ try:
84
+ from services.api.chat_endpoints import router as chat_router
85
+ except ImportError:
86
+ spec = importlib.util.spec_from_file_location(
87
+ "chat_endpoints",
88
+ os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "chat_endpoints.py")
89
+ )
90
+ chat_endpoints = importlib.util.module_from_spec(spec)
91
+ spec.loader.exec_module(chat_endpoints)
92
+ chat_router = chat_endpoints.router
93
+
94
+ # Debug print of available routes
95
+ print("DEBUG: Available routes in api_router:")
96
+ for route in api_router.routes:
97
+ print(f"Route: {route.path}, Methods: {route.methods}")
98
+
99
+ # Include API routers
100
+ app.include_router(api_router)
101
+ app.include_router(chat_router, prefix="/api")
102
+
103
+
104
+ class LoginPayload(BaseModel):
105
+ username: str
106
+ password: str
107
+ role: str
108
+
109
+ class PasswordChangePayload(BaseModel):
110
+ current_password: str
111
+ new_password: str
112
+
113
+ class PasswordChangePayload(BaseModel):
114
+ current_password: str
115
+ new_password: str
116
+
117
+ # Token functions moved to token_utils.py
118
+
119
+ @app.post("/login")
120
+ async def login(response: Response, payload: LoginPayload):
121
+ try:
122
+ print(f"Login attempt: {payload.username}, role: {payload.role}")
123
+
124
+ # Directly verify the user credentials
125
+ table_map = {
126
+ "Learner": "Learners",
127
+ "Instructor": "Instructors"
128
+ }
129
+ table = table_map.get(payload.role)
130
+ if not table:
131
+ print(f"Invalid role: {payload.role}")
132
+ raise HTTPException(status_code=400, detail="Invalid role")
133
+
134
+ conn = connect_db()
135
+ try:
136
+ with conn.cursor() as cur:
137
+ query = f"SELECT Password FROM {table} WHERE AccountName=%s LIMIT 1"
138
+ print(f"Executing query: {query} with username: {payload.username}")
139
+
140
+ cur.execute(query, (payload.username,))
141
+ row = cur.fetchone()
142
+
143
+ if not row:
144
+ print(f"No user found with username: {payload.username} in table: {table}")
145
+ raise HTTPException(status_code=401, detail="Incorrect username or password")
146
+
147
+ password_valid = check_password(payload.password, row[0])
148
+ print(f"Password check result: {password_valid}")
149
+
150
+ if not password_valid:
151
+ print(f"Authentication failed: Invalid password")
152
+ raise HTTPException(status_code=401, detail="Incorrect username or password")
153
+ except Exception as db_err:
154
+ print(f"Database error: {str(db_err)}")
155
+ raise HTTPException(status_code=500, detail=f"Database error: {str(db_err)}")
156
+ finally:
157
+ conn.close()
158
+
159
+ # User authenticated successfully
160
+ user_data = {"username": payload.username, "role": payload.role}
161
+ print(f"Authentication successful for: {payload.username}")
162
+
163
+ token = create_token(user_data)
164
+ response.set_cookie(
165
+ key="auth_token",
166
+ value=token,
167
+ httponly=False, # Cookie cannot be accessed by JavaScript
168
+ samesite="Lax", # More secure than None
169
+ secure=True, # Only send cookie over HTTPS
170
+ path="/",
171
+ max_age=604800 # 7 days
172
+ )
173
+ return {
174
+ "message": f"Login successful for {user_data['username']}",
175
+ "username": user_data["username"],
176
+ "role": user_data["role"],
177
+ "token": token
178
+ }
179
+ except Exception as e:
180
+ print(f"Login exception: {str(e)}")
181
+ raise HTTPException(status_code=500, detail=f"Login error: {str(e)}")
182
+
183
+ @app.get("/logout")
184
+ def logout(response: Response):
185
+ response.delete_cookie("auth_token")
186
+ return HTMLResponse("<h3>Logged out — cookie cleared!</h3>")
187
+
188
+ @app.get("/whoami")
189
+ async def whoami(auth_token: str = Cookie(None)):
190
+ payload = decode_token(auth_token)
191
+ return {"username": payload["username"], "role": payload["role"]}
192
+
193
+ @app.get("/protected")
194
+ def protected_route(auth_token: str = Cookie(None)):
195
+ payload = decode_token(auth_token)
196
+ return {"msg": "Access granted", "user": payload}
197
+
198
+ @app.put("/api/user/password")
199
+ async def change_password(payload: PasswordChangePayload, request: Request, auth_token: str = Cookie(None)):
200
+ """Change user password endpoint."""
201
+ # Get token from cookie or Authorization header
202
+ token = auth_token
203
+ if not token and "Authorization" in request.headers:
204
+ auth_header = request.headers["Authorization"]
205
+ if auth_header.startswith("Bearer "):
206
+ token = auth_header[7:] # Remove 'Bearer ' prefix
207
+
208
+ if not token:
209
+ raise HTTPException(status_code=401, detail="No authentication token provided")
210
+ try:
211
+ # Decode token to get user info
212
+ user_data = decode_token(token)
213
+ if not user_data:
214
+ raise HTTPException(status_code=401, detail="Invalid token")
215
+
216
+ username = user_data["username"]
217
+ role = user_data["role"]
218
+
219
+ # Get the correct table based on role
220
+ table_map = {
221
+ "Learner": "Learners",
222
+ "Instructor": "Instructors"
223
+ }
224
+ table = table_map.get(role)
225
+ if not table:
226
+ raise HTTPException(status_code=400, detail="Invalid role")
227
+
228
+ conn = connect_db()
229
+ try:
230
+ with conn.cursor() as cur:
231
+ # First verify current password
232
+ query = f"SELECT Password FROM {table} WHERE AccountName=%s LIMIT 1"
233
+ cur.execute(query, (username,))
234
+ row = cur.fetchone()
235
+
236
+ if not row or not check_password(payload.current_password, row[0]):
237
+ raise HTTPException(status_code=401, detail="Current password is incorrect")
238
+
239
+ # Hash the new password
240
+ new_password_hash = hash_password(payload.new_password)
241
+
242
+ # Update the password
243
+ update_query = f"UPDATE {table} SET Password=%s WHERE AccountName=%s"
244
+ cur.execute(update_query, (new_password_hash, username))
245
+ conn.commit()
246
+
247
+ return {"message": "Password updated successfully"}
248
+
249
+ except Exception as db_err:
250
+ conn.rollback()
251
+ print(f"Database error: {str(db_err)}")
252
+ raise HTTPException(status_code=500, detail=f"Database error: {str(db_err)}")
253
+ finally:
254
+ conn.close()
255
+
256
+ except HTTPException:
257
+ raise
258
+ except Exception as e:
259
+ print(f"Unexpected error in change_password: {str(e)}")
260
+ raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
261
+
262
+ def connect_db():
263
+ return pymysql.connect(
264
+ host=MYSQL_HOST,
265
+ user=MYSQL_USER,
266
+ password=MYSQL_PASSWORD,
267
+ database=MYSQL_DB,
268
+ port=MYSQL_PORT
269
+ )
270
+
271
+ def check_password(plain: str, hashed: str) -> bool:
272
+ """Compare plaintext vs bcrypt hash stored in DB."""
273
+ return bcrypt.checkpw(plain.encode("utf-8"), hashed.encode("utf-8"))
274
+
275
+ def hash_password(plain: str) -> str:
276
+ """Return bcrypt hash for storage."""
277
+ return bcrypt.hashpw(plain.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
278
+
279
+ # ───────────────────────────────────────────��────────────────────────────────────
280
+ # Pydantic models
281
+ # ────────────────────────────────────────────────────────────────────────────────
282
+ class UserCredentials(BaseModel):
283
+ username: str
284
+ password: str
285
+ role: str # "Learner" or "Instructor"
286
+
287
+ class NewUser(UserCredentials):
288
+ name: str
289
+ email: str
290
+
291
+
292
+ @app.post("/auth/verify_user")
293
+ def verify_user(creds: UserCredentials):
294
+ """
295
+ Validate user credentials.
296
+ Returns {username, role} on success; 401 on failure.
297
+ """
298
+ try:
299
+ print(f"Verify user attempt for: {creds.username}, role: {creds.role}")
300
+
301
+ table_map = {
302
+ "Learner": "Learners",
303
+ "Instructor": "Instructors"
304
+ }
305
+ table = table_map.get(creds.role)
306
+ if not table:
307
+ print(f"Invalid role: {creds.role}")
308
+ raise HTTPException(status_code=400, detail="Invalid role")
309
+
310
+ conn = connect_db()
311
+ try:
312
+ with conn.cursor() as cur:
313
+ query = f"SELECT Password FROM {table} WHERE AccountName=%s LIMIT 1"
314
+ print(f"Executing query: {query} with username: {creds.username}")
315
+
316
+ cur.execute(query, (creds.username,))
317
+ row = cur.fetchone()
318
+
319
+ if not row:
320
+ print(f"No user found with username: {creds.username} in table: {table}")
321
+ except Exception as db_err:
322
+ print(f"Database error: {str(db_err)}")
323
+ raise HTTPException(status_code=500, detail=f"Database error: {str(db_err)}")
324
+ finally:
325
+ conn.close()
326
+
327
+ if not row:
328
+ print(f"Authentication failed: User not found")
329
+ raise HTTPException(status_code=401, detail="Incorrect username or password")
330
+
331
+ password_valid = check_password(creds.password, row[0])
332
+ print(f"Password check result: {password_valid}")
333
+
334
+ if not password_valid:
335
+ print(f"Authentication failed: Invalid password")
336
+ raise HTTPException(status_code=401, detail="Incorrect username or password")
337
+
338
+ print(f"Authentication successful for: {creds.username}")
339
+ return {"username": creds.username, "role": creds.role}
340
+ except HTTPException:
341
+ raise
342
+ except Exception as e:
343
+ print(f"Unexpected error in verify_user: {str(e)}")
344
+ raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
345
+
346
+
347
+ @app.post("/auth/register_user")
348
+ def register_user(user: NewUser):
349
+ """
350
+ Simple sign-up endpoint.
351
+ Returns 201 Created on success, 409 if username/email exists.
352
+ """
353
+ table_cfg = {
354
+ "Learner": ("Learners", "LearnerName"),
355
+ "Instructor": ("Instructors", "InstructorName")
356
+ }
357
+ cfg = table_cfg.get(user.role)
358
+ if not cfg:
359
+ raise HTTPException(status_code=400, detail="Invalid role")
360
+
361
+ table, name_col = cfg
362
+ hashed_pw = hash_password(user.password)
363
+
364
+ conn = connect_db()
365
+ try:
366
+ with conn.cursor() as cur:
367
+ # Uniqueness check
368
+ cur.execute(
369
+ f"SELECT 1 FROM {table} WHERE AccountName=%s OR Email=%s LIMIT 1",
370
+ (user.username, user.email)
371
+ )
372
+ if cur.fetchone():
373
+ raise HTTPException(status_code=409, detail="Username or email already exists")
374
+
375
+ # Insert new record
376
+ cur.execute(
377
+ f"INSERT INTO {table} ({name_col}, Email, AccountName, Password) "
378
+ f"VALUES (%s, %s, %s, %s)",
379
+ (user.name, user.email, user.username, hashed_pw)
380
+ )
381
+ conn.commit()
382
+ finally:
383
+ conn.close()
384
+
385
+ return {"msg": "User created", "username": user.username, "role": user.role}, 201
386
+
387
+
388
+ @app.post("/api/auth/logout")
389
+ async def logout():
390
+ response = JSONResponse(content={"message": "Logged out successfully"})
391
+ # Clear all auth-related cookies
392
+ response.delete_cookie(key="auth_token", path="/")
393
+ response.delete_cookie(key="session_id", path="/")
394
+ return response
395
+
396
+ @app.get("/api/users")
397
+ async def get_users(role: str):
398
+ table_map = {
399
+ "Learner": "Learners",
400
+ "Instructor": "Instructors"
401
+ }
402
+ table = table_map.get(role)
403
+ if not table:
404
+ raise HTTPException(status_code=400, detail="Invalid role")
405
+
406
+ conn = connect_db()
407
+ try:
408
+ with conn.cursor() as cur:
409
+ cur.execute(f"SELECT * FROM {table}")
410
+ columns = [desc[0] for desc in cur.description]
411
+ users = [dict(zip(columns, row)) for row in cur.fetchall()]
412
+ return users
413
+ except Exception as e:
414
+ raise HTTPException(status_code=500, detail=str(e))
415
+ finally:
416
+ conn.close()
417
+
418
+ @app.get("/api/statistics/users/count")
419
+ async def get_user_count(role: str):
420
+ table_map = {
421
+ "Learner": "Learners",
422
+ "Instructor": "Instructors"
423
+ }
424
+ table = table_map.get(role)
425
+ if not table:
426
+ raise HTTPException(status_code=400, detail="Invalid role")
427
+
428
+ conn = connect_db()
429
+ try:
430
+ with conn.cursor() as cur:
431
+ cur.execute(f"SELECT COUNT(*) FROM {table}")
432
+ count = cur.fetchone()[0]
433
+ return {"count": count}
434
+ except Exception as e:
435
+ raise HTTPException(status_code=500, detail=str(e))
436
+ finally:
437
+ conn.close()
services/api/db/token_utils.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ token_utils.py - JWT token utilities for authentication
3
+ ------------------------------------------------------
4
+ This module contains functions for JWT token encoding and decoding
5
+ Extracted to avoid circular imports between auth.py and api_endpoints.py
6
+ """
7
+
8
+ from fastapi import HTTPException
9
+ from jose import jwt, JWTError
10
+ import time
11
+ import os
12
+ from dotenv import load_dotenv
13
+
14
+ # Load environment variables
15
+ load_dotenv()
16
+ SECRET_KEY = os.getenv("SECRET_TOKEN", "somesecretkey")
17
+ ALGORITHM = os.getenv("ALGORITHM", "HS256")
18
+
19
+ def create_token(data: dict, expires_in: int = 86400):
20
+ """
21
+ Create a JWT token from user data
22
+
23
+ Args:
24
+ data: Dictionary containing user information
25
+ expires_in: Token expiration time in seconds (default: 24 hours)
26
+
27
+ Returns:
28
+ JWT token string
29
+ """
30
+ to_encode = data.copy()
31
+ to_encode.update({"exp": time.time() + expires_in})
32
+ return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
33
+
34
+ def decode_token(token: str):
35
+ """
36
+ Decode and validate a JWT token
37
+
38
+ Args:
39
+ token: JWT token string
40
+
41
+ Returns:
42
+ Dictionary with decoded token data
43
+
44
+ Raises:
45
+ HTTPException: If token is invalid or expired
46
+ """
47
+ try:
48
+ return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
49
+ except JWTError:
50
+ raise HTTPException(status_code=403, detail="Invalid or expired token")
services/api/skills.csv ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ skill,
2
+ "Computer Science",
3
+ "Fundamentals",
4
+ "Advanced Techniques",
5
+ "Mathematics",
6
+ "Data Science",
7
+ "Economics",
8
+ "Finance",
9
+ "Marketing",
10
+ "Graphic Design",
11
+ "English Language",
12
+ "Project Management",
13
+ "Web Development",
14
+ "Artificial Intelligence",
15
+ "Cryptography",
16
+ "Network Security",
17
+ "Business Analysis",
18
+ "Human Resources",
19
+ "Digital Marketing",
20
+ "Entrepreneurship",
21
+ "Public Speaking"