talimbot / backend /main.py
parinazAkef's picture
fixed prompt percentages & updated vark link
2a1cd96
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
import json
import os
from pathlib import Path
from dotenv import load_dotenv
# Load environment variables from .env file (for local development)
load_dotenv()
app = FastAPI()
# CORS configuration - allow all origins for flexibility
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Data file path
DATA_FILE = Path(__file__).parent / "data" / "students.json"
DATA_FILE.parent.mkdir(exist_ok=True)
# Pydantic models
class Student(BaseModel):
studentNumber: str
name: str
nationalCode: str = ""
mbti: Optional[str] = None
learningStyle: Optional[str] = None
ams: Optional[str] = None
cooperative: Optional[str] = None
grade: float
preferredStudents: List[str] = []
group: Optional[int] = None
class StudentUpdate(BaseModel):
mbti: Optional[str] = None
learningStyle: Optional[str] = None
ams: Optional[str] = None
cooperative: Optional[str] = None
preferredStudents: Optional[List[str]] = None
class GroupingRequest(BaseModel):
courseName: str
class TeacherAuthRequest(BaseModel):
password: str
class StudentAuthRequest(BaseModel):
studentNumber: str
nationalCode: str
class SystemData(BaseModel):
students: List[Student]
courseName: str = ""
groupingComplete: bool = False
groupingResults: Optional[Dict[str, Any]] = None
resultsVisible: bool = False
teacherPassword: str = "teacher123"
# Initialize data
def load_data() -> SystemData:
if DATA_FILE.exists():
with open(DATA_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
return SystemData(**data)
else:
# Initialize with real 30 students from class data
initial_students = [
Student(studentNumber='S001', name='یاسمن آدینه پور', nationalCode='929986644', grade=18.77),
Student(studentNumber='S002', name='پریا احمدزاده', nationalCode='980085330', grade=17.28),
Student(studentNumber='S003', name='فاطمه اکبرزاده', nationalCode='970154550', grade=16.71),
Student(studentNumber='S004', name='آناهیتا الهی مهر', nationalCode='26425955', grade=19.05),
Student(studentNumber='S005', name='مریم امیری', nationalCode='980093341', grade=18.87),
Student(studentNumber='S006', name='باران برادران رحیمی', nationalCode='960043985', grade=19.07),
Student(studentNumber='S007', name='مایسا بصیری امین', nationalCode='960089446', grade=19.33),
Student(studentNumber='S008', name='دلارام ثابت عهد', nationalCode='960125620', grade=19.55),
Student(studentNumber='S009', name='شاینا جان محمدی', nationalCode='960068041', grade=19.47),
Student(studentNumber='S010', name='آیدا جوان', nationalCode='95112313', grade=16.77),
Student(studentNumber='S011', name='سارینا حاجی آبادی', nationalCode='999216751', grade=16.08),
Student(studentNumber='S012', name='هستی حسن پور جوان', nationalCode='960074198', grade=19.55),
Student(studentNumber='S013', name='فاطمه حسینی', nationalCode='2400410259', grade=19.07),
Student(studentNumber='S014', name='غزل خسروی', nationalCode='929995767', grade=15.05),
Student(studentNumber='S015', name='غزل ذباح', nationalCode='960110186', grade=19.25),
Student(studentNumber='S016', name='نازنین زهرا راشکی', nationalCode='3661516087', grade=17.02),
Student(studentNumber='S017', name='ویونا روح نواز', nationalCode='314458344', grade=18.70),
Student(studentNumber='S018', name='روژینا سعادتی', nationalCode='960051023', grade=18.20),
Student(studentNumber='S019', name='ترنم شعبانی', nationalCode='950083100', grade=19.37),
Student(studentNumber='S020', name='ستایش شفابخش', nationalCode='960126899', grade=18.36),
Student(studentNumber='S021', name='فاطمه شیرزادخان', nationalCode='980120756', grade=19.33),
Student(studentNumber='S022', name='آرزو علی جوی', nationalCode='960054316', grade=17.98),
Student(studentNumber='S023', name='آناهیتا قنادزاده', nationalCode='960089836', grade=18.84),
Student(studentNumber='S024', name='نیایش کارگر', nationalCode='929956052', grade=17.74),
Student(studentNumber='S025', name='باران کبریایی نسب', nationalCode='980119588', grade=18.82),
Student(studentNumber='S026', name='زینب کیانوش', nationalCode='970072678', grade=18.58),
Student(studentNumber='S027', name='ستایش محمودی', nationalCode='929904656', grade=19.33),
Student(studentNumber='S028', name='ستایش مشتاقی', nationalCode='361282217', grade=17.67),
Student(studentNumber='S029', name='مهتاب معلمی', nationalCode='960070265', grade=18.56),
Student(studentNumber='S030', name='باران وحدتی', nationalCode='929916913', grade=15.02),
]
data = SystemData(students=initial_students)
save_data(data)
return data
def save_data(data: SystemData):
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(data.dict(), f, ensure_ascii=False, indent=2)
# API Endpoints
@app.get("/")
def read_root():
return {"message": "TalimBot API is running"}
@app.get("/api/students")
def get_all_students():
data = load_data()
return {"students": data.students}
@app.get("/api/student/{student_number}")
def get_student(student_number: str):
# Return demo account if requested
if student_number == "DEMO":
return Student(
studentNumber="DEMO",
name="پریناز عاکف",
nationalCode="0921111111",
grade=0.0,
mbti=None,
learningStyle=None,
ams=None,
cooperative=None,
preferredStudents=[],
group=None
)
data = load_data()
student = next((s for s in data.students if s.studentNumber == student_number), None)
if not student:
raise HTTPException(status_code=404, detail="Student not found")
return student
@app.put("/api/student/{student_number}")
def update_student(student_number: str, updates: StudentUpdate):
# Silently ignore updates to demo account (pretend it worked)
if student_number == "DEMO":
demo_student = Student(
studentNumber="DEMO",
name="پریناز عاکف",
nationalCode="0921111111",
grade=0.0,
mbti=updates.mbti,
learningStyle=updates.learningStyle,
ams=updates.ams,
cooperative=updates.cooperative,
preferredStudents=updates.preferredStudents or [],
group=None
)
return {"success": True, "student": demo_student}
data = load_data()
student = next((s for s in data.students if s.studentNumber == student_number), None)
if not student:
raise HTTPException(status_code=404, detail="Student not found")
# Update only provided fields
update_dict = updates.dict(exclude_unset=True)
for key, value in update_dict.items():
setattr(student, key, value)
save_data(data)
return {"success": True, "student": student}
class GroupingRequest(BaseModel):
courseName: str
@app.post("/api/grouping/perform")
async def perform_grouping(request: GroupingRequest):
data = load_data()
# Get API key from environment variable
api_key = os.getenv("OPENROUTER_API_KEY")
if not api_key:
raise HTTPException(
status_code=500,
detail="OpenRouter API key not configured. Please set OPENROUTER_API_KEY in your environment variables or hosting platform settings"
)
# Get students with complete info (mbti and learningStyle required)
students_with_info = [s for s in data.students if s.mbti and s.learningStyle]
if len(students_with_info) == 0:
raise HTTPException(status_code=400, detail="No students have completed their profiles yet")
try:
# Import grouping logic
from grouping_logic import group_students_with_ai
# group_students_with_ai is now synchronous (uses requests library)
# Run it in a thread pool to avoid blocking
import asyncio
from concurrent.futures import ThreadPoolExecutor
loop = asyncio.get_event_loop()
with ThreadPoolExecutor() as executor:
grouping_result = await loop.run_in_executor(
executor,
lambda: group_students_with_ai(students_with_info, request.courseName, api_key)
)
# Apply grouping to students
for student in data.students:
student.group = None
for group in grouping_result["groups"]:
for student_number in group["students"]:
student = next((s for s in data.students if s.studentNumber == student_number), None)
if student:
student.group = group["groupNumber"]
data.courseName = request.courseName
data.groupingComplete = True
data.groupingResults = grouping_result
data.resultsVisible = False # Teacher must manually show results
save_data(data)
return {"success": True, "results": grouping_result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/grouping/status")
def get_grouping_status():
data = load_data()
students_with_info = [s for s in data.students if s.mbti and s.learningStyle]
students_grouped = [s for s in data.students if s.group is not None]
return {
"totalStudents": len(data.students),
"studentsWithCompleteInfo": len(students_with_info),
"studentsGrouped": len(students_grouped),
"groupingComplete": data.groupingComplete,
"resultsVisible": data.resultsVisible,
"courseName": data.courseName,
"groups": data.groupingResults.get("groups", []) if data.groupingResults else []
}
class PasswordRequest(BaseModel):
password: str
@app.post("/api/grouping/toggle-visibility")
def toggle_results_visibility(request: PasswordRequest):
data = load_data()
if request.password != data.teacherPassword:
raise HTTPException(status_code=403, detail="Invalid password")
data.resultsVisible = not data.resultsVisible
save_data(data)
return {"success": True, "resultsVisible": data.resultsVisible}
@app.post("/api/grouping/reset")
def reset_grouping(request: PasswordRequest):
data = load_data()
if request.password != data.teacherPassword:
raise HTTPException(status_code=403, detail="Invalid password")
# Clear ONLY grouping-related data, keep student profiles intact
for student in data.students:
student.group = None
data.groupingComplete = False
data.groupingResults = None
data.resultsVisible = False
data.courseName = ""
save_data(data)
return {"success": True}
@app.post("/api/data/reset-all")
def reset_all_data(request: PasswordRequest):
data = load_data()
if request.password != data.teacherPassword:
raise HTTPException(status_code=403, detail="Invalid password")
# Clear ALL student data fields AND grouping
for student in data.students:
student.group = None
student.mbti = None
student.learningStyle = None
student.ams = None
student.cooperative = None
student.preferredStudents = []
data.groupingComplete = False
data.groupingResults = None
data.resultsVisible = False
data.courseName = ""
save_data(data)
return {"success": True}
@app.post("/api/auth/teacher")
def check_teacher_password(request: TeacherAuthRequest):
data = load_data()
return {"valid": request.password == data.teacherPassword}
@app.post("/api/auth/student")
def authenticate_student(request: StudentAuthRequest):
# Special demo account - not stored in database
# Check for national code without leading zero (frontend strips it)
if request.nationalCode == "921111111":
demo_student = Student(
studentNumber="DEMO",
name="پریناز عاکف",
nationalCode="921111111",
grade=0.0,
mbti=None,
learningStyle=None,
ams=None,
cooperative=None,
preferredStudents=[],
group=None
)
return {"valid": True, "student": demo_student}
data = load_data()
student = next((s for s in data.students if s.studentNumber == request.studentNumber), None)
if not student:
raise HTTPException(status_code=404, detail="Student not found")
if student.nationalCode != request.nationalCode:
raise HTTPException(status_code=401, detail="Invalid national code")
return {"valid": True, "student": student}
class NationalCodeAuthRequest(BaseModel):
nationalCode: str
@app.post("/api/auth/student-by-nationalcode")
def authenticate_student_by_nationalcode(request: NationalCodeAuthRequest):
# Special demo account - not stored in database
# Check for national code without leading zero (frontend strips it)
if request.nationalCode == "921111111":
demo_student = Student(
studentNumber="DEMO",
name="پریناز عاکف",
nationalCode="921111111",
grade=0.0,
mbti=None,
learningStyle=None,
ams=None,
cooperative=None,
preferredStudents=[],
group=None
)
return {"valid": True, "student": demo_student}
data = load_data()
# Find student by national code (without leading zero)
student = next((s for s in data.students if s.nationalCode == request.nationalCode), None)
if not student:
raise HTTPException(status_code=404, detail="کد ملی در سیستم یافت نشد")
return {"valid": True, "student": student}
@app.get("/api/student/{student_number}/group")
def get_student_group(student_number: str):
# Demo account has no group
if student_number == "DEMO":
raise HTTPException(status_code=404, detail="Demo account is not part of any group")
data = load_data()
if not data.resultsVisible:
raise HTTPException(status_code=403, detail="Results are not yet visible")
student = next((s for s in data.students if s.studentNumber == student_number), None)
if not student:
raise HTTPException(status_code=404, detail="Student not found")
if student.group is None:
raise HTTPException(status_code=404, detail="Student not assigned to a group yet")
# Find all students in the same group
group_members = [s for s in data.students if s.group == student.group]
# Find the group details from results
group_info = None
if data.groupingResults:
for g in data.groupingResults.get("groups", []):
if g["groupNumber"] == student.group:
group_info = g
break
return {
"groupNumber": student.group,
"members": group_members,
"reasoning": group_info.get("reasoning", "") if group_info else "",
"courseName": data.courseName
}
@app.get("/api/data/backup")
def get_data_backup():
"""Download complete student data as JSON backup for safekeeping"""
data = load_data()
return data.dict()
# ============================================
# STATIC FILE SERVING (Frontend HTML/CSS/JS)
# ============================================
# Define static directory path
STATIC_DIR = Path(__file__).parent / "static"
# Mount static files FIRST - this handles all non-API routes
app.mount("/", StaticFiles(directory=str(STATIC_DIR), html=True), name="static")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)