sajith-0701's picture
prepare for huggingface deployment
3d40e5f
import json
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, Form
from fastapi.responses import StreamingResponse
from auth.jwt import require_role, get_current_user
from schemas.admin import (
JobRoleCreate, JobRoleUpdate,
QuestionCreate, QuestionUpdate,
RoleRequirementCreate,
TopicCreate, TopicUpdate, TopicPublishUpdate,
GroupTestCreate, GroupTestUpdate, GroupTestPublishUpdate,
ChatbotQueryRequest, ChatbotExportRequest, ChatbotStudentUpdate,
)
from services.admin_service import (
create_role, update_role, delete_role, list_roles,
create_question, update_question, delete_question, list_questions, get_question_by_id,
create_topic, list_topics, update_topic, delete_topic, set_topic_publish_status,
import_questions_from_pdf,
create_requirement, list_requirements, delete_requirement,
list_quit_interviews, list_admin_reports, get_admin_report_detail,
list_admin_users, delete_admin_user,
)
from services.job_description_service import (
create_job_description,
list_admin_job_descriptions,
update_admin_job_description,
delete_admin_job_description,
parse_jd_from_file,
)
from services.group_test_service import (
create_group_test,
list_group_tests,
get_group_test,
update_group_test,
delete_group_test,
set_group_test_publish,
get_group_test_results_admin,
)
from services.analytics_service import get_admin_analytics
router = APIRouter()
# ─── Job Roles ───
@router.get("/roles")
async def get_roles(current_user: dict = Depends(get_current_user)):
"""List all job roles (accessible by all authenticated users for interview selection)."""
roles = await list_roles()
return {"roles": roles}
@router.post("/roles")
async def create_role_endpoint(
request: JobRoleCreate,
current_user: dict = Depends(require_role("admin")),
):
"""Create a new job role (admin only)."""
result = await create_role(
title=request.title,
description=request.description,
department=request.department,
)
return result
@router.put("/roles/{role_id}")
async def update_role_endpoint(
role_id: str,
request: JobRoleUpdate,
current_user: dict = Depends(require_role("admin")),
):
"""Update a job role (admin only)."""
try:
result = await update_role(role_id, request.model_dump())
return result
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/roles/{role_id}")
async def delete_role_endpoint(
role_id: str,
current_user: dict = Depends(require_role("admin")),
):
"""Delete a job role (admin only)."""
success = await delete_role(role_id)
if not success:
raise HTTPException(status_code=404, detail="Role not found")
return {"message": "Role deleted"}
# ─── Questions ───
@router.get("/questions")
async def get_questions(
role_id: str = Query(None),
topic_id: str = Query(None),
interview_type: str = Query(None),
difficulty: str = Query(None),
current_user: dict = Depends(require_role("admin")),
):
"""List questions, optionally filtered by role."""
questions = await list_questions(
role_id=role_id,
topic_id=topic_id,
interview_type=interview_type,
difficulty=difficulty,
)
return {"questions": questions}
@router.post("/questions")
async def create_question_endpoint(
request: QuestionCreate,
current_user: dict = Depends(require_role("admin")),
):
"""Create a new question (admin only)."""
result = await create_question(
role_id=request.role_id,
topic_id=request.topic_id,
interview_type=request.interview_type,
question=request.question,
difficulty=request.difficulty,
category=request.category,
expected_answer=request.expected_answer,
)
return result
@router.get("/questions/{question_id}")
async def get_question_by_id_endpoint(
question_id: str,
current_user: dict = Depends(require_role("admin")),
):
"""Get one question by id (admin only)."""
try:
question = await get_question_by_id(question_id)
return question
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@router.post("/questions/upload")
async def upload_questions_pdf_endpoint(
interview_type: str = Form("resume"),
role_id: str | None = Form(None),
topic_id: str | None = Form(None),
subjects: str | None = Form(None),
file: UploadFile = File(...),
current_user: dict = Depends(require_role("admin")),
):
"""Upload a PDF and extract interview questions (admin only)."""
if not file.filename:
raise HTTPException(status_code=400, detail="No file provided")
if not file.filename.lower().endswith(".pdf"):
raise HTTPException(status_code=400, detail="Only PDF files are supported for question import")
content = await file.read()
if len(content) > 10 * 1024 * 1024:
raise HTTPException(status_code=400, detail="File too large. Maximum 10MB")
parsed_subjects = []
if subjects:
try:
parsed_subjects = json.loads(subjects)
if not isinstance(parsed_subjects, list):
raise ValueError
except Exception:
parsed_subjects = [s.strip() for s in subjects.split(",") if s.strip()]
try:
result = await import_questions_from_pdf(
role_id=role_id,
topic_id=topic_id,
interview_type=interview_type,
subjects=parsed_subjects,
filename=file.filename,
file_content=content,
)
return result
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to import questions from PDF: {str(e)}")
@router.put("/questions/{question_id}")
async def update_question_endpoint(
question_id: str,
request: QuestionUpdate,
current_user: dict = Depends(require_role("admin")),
):
"""Update a question (admin only)."""
try:
result = await update_question(question_id, request.model_dump())
return result
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/questions/{question_id}")
async def delete_question_endpoint(
question_id: str,
current_user: dict = Depends(require_role("admin")),
):
"""Delete a question (admin only)."""
success = await delete_question(question_id)
if not success:
raise HTTPException(status_code=404, detail="Question not found")
return {"message": "Question deleted"}
# ─── Role Requirements ───
@router.get("/requirements/{role_id}")
async def get_requirements(
role_id: str,
current_user: dict = Depends(require_role("admin")),
):
"""List requirements for a role."""
requirements = await list_requirements(role_id)
return {"requirements": requirements}
# ─── Topics ───
@router.get("/topics")
async def get_topics(current_user: dict = Depends(get_current_user)):
"""List all topic categories (accessible by all authenticated users)."""
only_published = current_user.get("role") != "admin"
topics = await list_topics(only_published=only_published)
return {"topics": topics}
@router.post("/topics")
async def create_topic_endpoint(
request: TopicCreate,
current_user: dict = Depends(require_role("admin")),
):
"""Create a topic category (admin only)."""
try:
result = await create_topic(name=request.name, description=request.description)
return result
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/topics/{topic_id}")
async def update_topic_endpoint(
topic_id: str,
request: TopicUpdate,
current_user: dict = Depends(require_role("admin")),
):
"""Update a topic category (admin only)."""
try:
result = await update_topic(topic_id, request.model_dump())
return result
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/topics/{topic_id}")
async def delete_topic_endpoint(
topic_id: str,
current_user: dict = Depends(require_role("admin")),
):
"""Delete a topic category and its topic questions (admin only)."""
success = await delete_topic(topic_id)
if not success:
raise HTTPException(status_code=404, detail="Topic not found")
return {"message": "Topic deleted"}
@router.put("/topics/{topic_id}/publish")
async def publish_topic_endpoint(
topic_id: str,
request: TopicPublishUpdate,
current_user: dict = Depends(require_role("admin")),
):
"""Publish/unpublish a topic for student interview selection (admin only)."""
try:
result = await set_topic_publish_status(
topic_id,
request.is_published,
timer_enabled=request.timer_enabled,
timer_seconds=request.timer_seconds,
)
return result
except ValueError as e:
detail = str(e)
status_code = 404 if "not found" in detail.lower() else 400
raise HTTPException(status_code=status_code, detail=detail)
@router.post("/requirements")
async def create_requirement_endpoint(
request: RoleRequirementCreate,
current_user: dict = Depends(require_role("admin")),
):
"""Create a role requirement (admin only)."""
result = await create_requirement(
role_id=request.role_id,
skill=request.skill,
level=request.level,
)
return result
@router.delete("/requirements/{req_id}")
async def delete_requirement_endpoint(
req_id: str,
current_user: dict = Depends(require_role("admin")),
):
"""Delete a role requirement (admin only)."""
success = await delete_requirement(req_id)
if not success:
raise HTTPException(status_code=404, detail="Requirement not found")
return {"message": "Requirement deleted"}
# ─── Analytics ───
@router.get("/analytics")
async def get_analytics(
current_user: dict = Depends(require_role("admin")),
):
"""Get admin analytics dashboard data."""
analytics = await get_admin_analytics()
return analytics
@router.get("/quit-interviews")
async def get_quit_interviews(
limit: int = Query(100, ge=1, le=500),
current_user: dict = Depends(require_role("admin")),
):
"""Get full details about interviews quit by users."""
items = await list_quit_interviews(limit=limit)
return {"items": items}
@router.get("/reports")
async def get_admin_reports(
limit: int = Query(100, ge=1, le=500),
current_user: dict = Depends(require_role("admin")),
):
"""Get all interview report summaries for admin."""
items = await list_admin_reports(limit=limit)
return {"items": items}
@router.get("/reports/{session_id}")
async def get_admin_report_by_session(
session_id: str,
current_user: dict = Depends(require_role("admin")),
):
"""Get full report details for a specific interview session (admin only)."""
try:
item = await get_admin_report_detail(session_id=session_id)
return item
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@router.get("/users")
async def get_admin_users(
limit: int = Query(500, ge=1, le=1000),
current_user: dict = Depends(require_role("admin")),
):
"""List users for admin management."""
items = await list_admin_users(limit=limit)
return {"items": items}
@router.get("/job-descriptions")
async def get_admin_job_descriptions(
owner_user_id: str = Query(None),
current_user: dict = Depends(require_role("admin")),
):
"""List job descriptions for admin management."""
items = await list_admin_job_descriptions(owner_user_id=owner_user_id)
return {"items": items}
@router.post("/job-descriptions")
async def create_admin_job_description_endpoint(
request_data: dict,
current_user: dict = Depends(require_role("admin")),
):
"""Create a job description as admin."""
try:
item = await create_job_description(
user_id=current_user["user_id"],
owner_role="admin",
title=request_data.get("title"),
company=request_data.get("company"),
description=request_data.get("description"),
required_skills=request_data.get("required_skills"),
)
return item
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/job-descriptions/{jd_id}")
async def update_admin_job_description_endpoint(
jd_id: str,
request_data: dict,
current_user: dict = Depends(require_role("admin")),
):
"""Update any job description (admin only)."""
try:
item = await update_admin_job_description(jd_id, request_data)
return item
except ValueError as e:
status_code = 404 if "not found" in str(e).lower() else 400
raise HTTPException(status_code=status_code, detail=str(e))
@router.delete("/job-descriptions/{jd_id}")
async def delete_admin_job_description_endpoint(
jd_id: str,
current_user: dict = Depends(require_role("admin")),
):
"""Delete any job description (admin only)."""
success = await delete_admin_job_description(jd_id)
if not success:
raise HTTPException(status_code=404, detail="Job description not found")
return {"message": "Job description deleted"}
@router.post("/job-descriptions/parse-file")
async def parse_admin_jd_file(
file: UploadFile = File(...),
current_user: dict = Depends(require_role("admin")),
):
"""Upload a JD file (PDF/DOCX/TXT) and extract structured fields via AI (admin only)."""
if not file.filename:
raise HTTPException(status_code=400, detail="No file provided")
allowed_ext = {".pdf", ".doc", ".docx", ".txt"}
ext = "." + file.filename.rsplit(".", 1)[-1].lower() if "." in file.filename else ""
if ext not in allowed_ext:
raise HTTPException(status_code=400, detail="Unsupported file type. Allowed: PDF, DOC, DOCX, TXT")
content = await file.read()
if len(content) > 10 * 1024 * 1024:
raise HTTPException(status_code=400, detail="File too large. Maximum 10MB")
try:
result = await parse_jd_from_file(file.filename, content)
return result
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to parse JD file: {str(e)}")
@router.delete("/users/{user_id}")
async def delete_admin_user_endpoint(
user_id: str,
current_user: dict = Depends(require_role("admin")),
):
"""Delete a student user and related records (admin only)."""
try:
success = await delete_admin_user(user_id, current_user["user_id"])
if not success:
raise HTTPException(status_code=404, detail="User not found")
return {"message": "User deleted"}
except ValueError as e:
detail = str(e)
status_code = 404 if "not found" in detail.lower() else 400
raise HTTPException(status_code=status_code, detail=detail)
# ─── Group Tests ─────────────────────────────────────────────────────────────
@router.get("/group-tests")
async def list_group_tests_endpoint(
current_user: dict = Depends(require_role("admin")),
):
items = await list_group_tests(only_published=False)
return {"items": items}
@router.post("/group-tests")
async def create_group_test_endpoint(
request: GroupTestCreate,
current_user: dict = Depends(require_role("admin")),
):
try:
result = await create_group_test(
name=request.name,
description=request.description,
topic_ids=request.topic_ids,
time_limit_minutes=request.time_limit_minutes,
max_attempts=request.max_attempts,
created_by=current_user["user_id"],
)
return result
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/group-tests/{group_test_id}")
async def get_group_test_endpoint(
group_test_id: str,
current_user: dict = Depends(require_role("admin")),
):
try:
return await get_group_test(group_test_id)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@router.put("/group-tests/{group_test_id}")
async def update_group_test_endpoint(
group_test_id: str,
request: GroupTestUpdate,
current_user: dict = Depends(require_role("admin")),
):
try:
return await update_group_test(group_test_id, request.model_dump(exclude_none=True))
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/group-tests/{group_test_id}")
async def delete_group_test_endpoint(
group_test_id: str,
current_user: dict = Depends(require_role("admin")),
):
success = await delete_group_test(group_test_id)
if not success:
raise HTTPException(status_code=404, detail="Group test not found")
return {"message": "Group test deleted"}
@router.patch("/group-tests/{group_test_id}/publish")
async def publish_group_test_endpoint(
group_test_id: str,
request: GroupTestPublishUpdate,
current_user: dict = Depends(require_role("admin")),
):
try:
return await set_group_test_publish(group_test_id, request.is_published)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@router.get("/group-tests/{group_test_id}/results")
async def get_group_test_results_endpoint(
group_test_id: str,
current_user: dict = Depends(require_role("admin")),
):
results = await get_group_test_results_admin(group_test_id)
return {"items": results}
# ─── Chatbot ──────────────────────────────────────────────────────────────────
from services.chatbot_service import (
process_chatbot_query,
update_student_info,
generate_excel,
filter_students_structured,
)
from schemas.admin import StudentFilterRequest, StudentFilterExportRequest
@router.post("/students/filter")
async def students_filter(
request: StudentFilterRequest,
current_user: dict = Depends(require_role("admin")),
):
"""Structured student filter β€” no AI, direct params (sort + date range + multi-test)."""
try:
result = await filter_students_structured(
group_test_ids=request.group_test_ids,
jd_id=request.jd_id,
start_date=request.start_date,
end_date=request.end_date,
top_k=request.top_k,
min_score=request.min_score,
sort_fields=request.sort_fields,
sort_orders=request.sort_orders,
)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/students/export-excel")
async def students_export_excel(
request: StudentFilterExportRequest,
current_user: dict = Depends(require_role("admin")),
):
"""Generate styled Excel for structured student filter results."""
try:
bio = generate_excel(
rows=request.rows,
topic_columns=request.topic_columns,
group_test_name=request.group_test_name,
)
safe_name = request.group_test_name.replace(" ", "_").replace("/", "-")[:40]
filename = f"{safe_name}_students.xlsx"
return StreamingResponse(
bio,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.patch("/students")
async def update_student(
request: ChatbotStudentUpdate,
current_user: dict = Depends(require_role("admin")),
):
"""Admin corrects a student's reg_no or name (shared by both filter endpoints)."""
try:
return await update_student_info(request.user_id, request.reg_no, request.name)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/chatbot/query")
async def chatbot_query(
request: ChatbotQueryRequest,
current_user: dict = Depends(require_role("admin")),
):
"""AI-powered student filter β€” returns ranked student rows."""
try:
result = await process_chatbot_query(request.query, request.jd_id)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/chatbot/export-excel")
async def chatbot_export_excel(
request: ChatbotExportRequest,
current_user: dict = Depends(require_role("admin")),
):
"""Generate styled Excel (.xlsx) from current chatbot result rows."""
try:
bio = generate_excel(
rows=request.rows,
topic_columns=request.topic_columns,
group_test_name=request.group_test_name,
)
safe_name = request.group_test_name.replace(" ", "_").replace("/", "-")[:40]
filename = f"{safe_name}_students.xlsx"
return StreamingResponse(
bio,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.patch("/chatbot/students")
async def chatbot_update_student(
request: ChatbotStudentUpdate,
current_user: dict = Depends(require_role("admin")),
):
"""Admin corrects a student's reg_no or name."""
try:
return await update_student_info(request.user_id, request.reg_no, request.name)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))