| import asyncio
|
| from concurrent.futures import ThreadPoolExecutor
|
| from fastapi import FastAPI, status
|
| from fastapi.middleware.cors import CORSMiddleware
|
| from fastapi.responses import JSONResponse
|
| from pydantic import BaseModel, Field
|
|
|
|
|
|
|
|
|
| import traceback
|
| try:
|
|
|
| from model_main import ContextRetriever, get_ai_grade, grade_batch
|
|
|
| from model_main import qdrant_manager, embedding_model
|
|
|
|
|
| retriever = ContextRetriever(qdrant=qdrant_manager, embedding=embedding_model)
|
| SYSTEM_READY = True
|
| print("🏆 [RAG-SERVER] Khởi tạo lõi RAG và LLM Judge thành công!")
|
| except Exception as e:
|
| SYSTEM_READY = False
|
| print("⚠️ [CẢNH BÁO] HỆ THỐNG LÕI ĐÃ SẬP! NGUYÊN NHÂN CHÍNH XÁC LÀ:")
|
| traceback.print_exc()
|
|
|
|
|
|
|
|
|
| app = FastAPI(
|
| title="AI Essay Judge Server (RAG + Qwen-72B)",
|
| description="Server nhận bài văn, tự động tìm tài liệu uy tín trên GDrive/Qdrant và chấm điểm.",
|
| version="2.0.0"
|
| )
|
|
|
|
|
| app.add_middleware(
|
| CORSMiddleware,
|
| allow_origins=["*"],
|
| allow_credentials=True,
|
| allow_methods=["*"],
|
| allow_headers=["*"],
|
| )
|
|
|
|
|
| executor = ThreadPoolExecutor(max_workers=10)
|
|
|
|
|
|
|
|
|
| class EssayInput(BaseModel):
|
| title: str = Field(default="Chưa có tiêu đề", description="Tiêu đề bài văn hoặc đề bài")
|
| content: str = Field(..., min_length=20, description="Nội dung bài văn của học sinh cần chấm")
|
|
|
| class BatchEssayInput(BaseModel):
|
| essays: list[EssayInput] = Field(..., description="Danh sách nhiều bài văn cần chấm cùng lúc")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @app.get("/health")
|
| def health_check():
|
| """Kiểm tra trạng thái hoạt động của Server"""
|
| return {
|
| "status": "healthy" if SYSTEM_READY else "unhealthy",
|
| "mode": "RAG_LLM_JUDGE_INTEGRATED",
|
| "ready": SYSTEM_READY
|
| }
|
|
|
| @app.post("/api/v1/judge")
|
| async def judge_single_essay(payload: EssayInput):
|
| """
|
| ENDPOINT CHÍNH: Nhận 1 bài văn -> Tìm đáp án (RAG) -> Chấm điểm (LLM Judge)
|
| """
|
| if not SYSTEM_READY:
|
| return JSONResponse(
|
| status_code=503,
|
| content={"error": "Hệ thống chưa sẵn sàng do lỗi cấu hình lõi AI."}
|
| )
|
|
|
| try:
|
| loop = asyncio.get_running_loop()
|
| bai_van = payload.content
|
|
|
|
|
|
|
| print(f"🔍 [RAG] Đang tìm kiếm tài liệu chuẩn cho bài viết: '{payload.title}'...")
|
| tai_lieu_chuan = await loop.run_in_executor(
|
| executor, retriever.search_context, bai_van, 3
|
| )
|
|
|
| if not tai_lieu_chuan or tai_lieu_chuan.strip() == "":
|
| print("⚠️ [RAG] Không tìm thấy tài liệu phù hợp! Hệ thống sẽ chuyển sang chấm tự do.")
|
| tai_lieu_chuan = "Không có tài liệu đáp án cụ thể. Hãy chấm điểm dựa trên kiến thức phổ thông chuẩn."
|
|
|
|
|
| print("🤖 [JUDGE] Đang gửi dữ liệu sang Qwen-72B để chấm điểm và export JSON...")
|
| ket_qua_json = await loop.run_in_executor(
|
| executor, get_ai_grade, bai_van, tai_lieu_chuan
|
| )
|
|
|
|
|
| return JSONResponse(status_code=200, content={"status": "success", "data": ket_qua_json})
|
|
|
| except Exception as e:
|
| err_str = str(e)
|
|
|
| if 'response_format' in err_str or 'BadRequestError' in err_str or 'Chưa cấu hình API key' in err_str:
|
| fallback = {
|
| "diem": 0.0,
|
| "xep_loai": "Không chấm được",
|
| "nhan_xet_chung": f"Lỗi LLM: {err_str}",
|
| "uu_diem": [],
|
| "nhuoc_diem": [],
|
| "chi_tiet_diem": {"noi_dung": 0.0, "hinh_thuc": 0.0, "sang_tao": 0.0},
|
| "ket_luan": "Kết quả tạm thời do lỗi hệ thống LLM."
|
| }
|
| return JSONResponse(status_code=200, content={"status": "fallback", "data": fallback})
|
|
|
| return JSONResponse(
|
| status_code=500,
|
| content={"error": "Lỗi xử lý chấm điểm nội bộ", "detail": err_str}
|
| )
|
|
|
| @app.post("/api/v1/judge-batch")
|
| async def judge_multiple_essays(payload: BatchEssayInput):
|
| """
|
| ENDPOINT BATCH: Chấm hàng loạt bài văn cùng lúc một cách tối ưu
|
| """
|
| if not SYSTEM_READY:
|
| return JSONResponse(status_code=503, content={"error": "Hệ thống chưa sẵn sàng."})
|
|
|
| try:
|
| loop = asyncio.get_running_loop()
|
|
|
| print(f"📦 [BATCH] Đang chuẩn bị trích xuất RAG song song cho {len(payload.essays)} bài văn...")
|
| bai_van_list = [essay.content for essay in payload.essays]
|
|
|
|
|
| def run_rag(content):
|
| return retriever.search_context(content, limit=3)
|
|
|
|
|
| rag_tasks = [loop.run_in_executor(executor, run_rag, content) for content in bai_van_list]
|
| tai_lieu_chuan_list = await asyncio.gather(*rag_tasks)
|
|
|
| print("🤖 [BATCH] Kích hoạt hàm grade_batch() chấm tuần tự để tránh nghẽn/rate limit API...")
|
| list_ket_qua = await loop.run_in_executor(
|
| executor, grade_batch, bai_van_list, tai_lieu_chuan_list
|
| )
|
|
|
| return JSONResponse(status_code=200, content={"results": list_ket_qua})
|
|
|
| except Exception as e:
|
| err_str = str(e)
|
| if 'response_format' in err_str or 'BadRequestError' in err_str:
|
|
|
| list_ket_qua = [
|
| {
|
| "diem": 0.0,
|
| "xep_loai": "Không chấm được",
|
| "nhan_xet_chung": f"Lỗi LLM: {err_str}",
|
| "uu_diem": [],
|
| "nhuoc_diem": [],
|
| "chi_tiet_diem": {"noi_dung": 0.0, "hinh_thuc": 0.0, "sang_tao": 0.0},
|
| "ket_luan": "Kết quả tạm thời do lỗi hệ thống LLM."
|
| }
|
| for _ in bai_van_list
|
| ]
|
| return JSONResponse(status_code=200, content={"results": list_ket_qua})
|
|
|
| return JSONResponse(status_code=500, content={"error": "Lỗi xử lý Batch nội bộ", "detail": err_str})
|
|
|
|
|
|
|
|
|
|
|
| if __name__ == "__main__":
|
| import uvicorn
|
|
|
| uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=False)
|
|
|