Spaces:
Sleeping
Sleeping
| """ | |
| FastAPI Application - Entry point cho Embedding API. | |
| Ứng dụng này cung cấp REST API để trích xuất vector nhúng | |
| từ model embedding-gemma-300m đã được fine-tune. | |
| Luồng hoạt động: | |
| 1. Khi app khởi động → load model 1 lần duy nhất vào RAM | |
| 2. Nhận request POST /embed → trích xuất embedding → trả về JSON | |
| 3. Health check GET / → xác nhận API đang hoạt động | |
| """ | |
| from contextlib import asynccontextmanager | |
| from fastapi import FastAPI, HTTPException, Depends, Security | |
| from fastapi.security import APIKeyHeader | |
| import traceback | |
| import gradio as gr | |
| from app.gradio_ui import create_gradio_interface | |
| from app.config import settings | |
| from app.model_loader import ( | |
| load_model, generate_embeddings, | |
| load_minilm, generate_embeddings_minilm, | |
| compute_cosine_similarity, | |
| ) | |
| from app.schemas import ( | |
| EmbeddingRequest, EmbeddingResponse, HealthResponse, | |
| SimilarityTextRequest, SimilarityVectorRequest, SimilarityResponse, | |
| ) | |
| # ==================== LIFESPAN (Load model 1 lần) ==================== | |
| async def lifespan(app: FastAPI): | |
| """ | |
| Quản lý vòng đời của ứng dụng. | |
| - Startup: Tải model vào bộ nhớ (chạy 1 lần duy nhất) | |
| - Shutdown: Giải phóng tài nguyên (nếu cần) | |
| """ | |
| # === STARTUP === | |
| print("=" * 50) | |
| print("[STARTUP] Khoi dong Embedding API...") | |
| print("=" * 50) | |
| load_model() | |
| load_minilm() | |
| print("=" * 50) | |
| print("[OK] All models loaded! API ready.") | |
| print("=" * 50) | |
| yield # App dang chay va phuc vu request tai day | |
| # === SHUTDOWN === | |
| print("[SHUTDOWN] Dang tat API...") | |
| # ==================== KHỞI TẠO FASTAPI APP ==================== | |
| app = FastAPI( | |
| title=settings.API_TITLE, | |
| description=settings.API_DESCRIPTION, | |
| version=settings.API_VERSION, | |
| lifespan=lifespan, | |
| ) | |
| # ==================== AUTHENTICATION ==================== | |
| api_key_header = APIKeyHeader(name="X-API-KEY", auto_error=False) | |
| async def get_api_key(api_key: str = Security(api_key_header)): | |
| """Kiểm tra X-API-KEY header của request.""" | |
| if not settings.API_SECRET_KEY: | |
| # Lỗi lập trình viên/DevOps quên cấu hình biến môi trường | |
| raise HTTPException( | |
| status_code=500, | |
| detail="Server Security Error: API_SECRET_KEY is not configured on the server.", | |
| ) | |
| if api_key != settings.API_SECRET_KEY: | |
| raise HTTPException( | |
| status_code=401, | |
| detail="Unauthorized: X-API-KEY is missing or invalid", | |
| ) | |
| return api_key | |
| # ==================== ENDPOINTS ==================== | |
| async def health_check(): | |
| """ | |
| Endpoint Health Check - GET /health | |
| Trả về trạng thái hoạt động của API. | |
| Dùng để xác nhận API đang chạy trước khi gửi request embedding. | |
| """ | |
| return HealthResponse( | |
| status="ok", | |
| model=f"{settings.MODEL_NAME} + {settings.MODEL_NAME_MINILM}", | |
| ) | |
| async def create_embedding(request: EmbeddingRequest, api_key: str = Depends(get_api_key)): | |
| """ | |
| Endpoint Embedding - POST /embed | |
| Nhận 1 hoặc nhiều văn bản, truyền qua model đã load sẵn, | |
| và trả về các vector nhúng tương ứng dưới dạng JSON. | |
| Hỗ trợ 2 kiểu input: | |
| - Single: {"text": "Xin chào"} | |
| - Batch: {"texts": ["Xin chào", "Hello"]} | |
| """ | |
| # Validate input | |
| texts = request.get_texts() | |
| if not texts: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Vui lòng cung cấp 'text' (string) hoặc 'texts' (list[string])", | |
| ) | |
| # Gọi hàm generate embeddings từ model đã load sẵn | |
| try: | |
| embeddings = generate_embeddings(texts) | |
| except Exception as e: | |
| traceback.print_exc() | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Error generating embedding: {str(e)}", | |
| ) | |
| # Trả kết quả | |
| return EmbeddingResponse( | |
| embeddings=embeddings, | |
| dimension=len(embeddings[0]) if embeddings else 0, | |
| num_texts=len(texts), | |
| ) | |
| async def compute_text_similarity(request: SimilarityTextRequest, api_key: str = Depends(get_api_key)): | |
| """ | |
| Endpoint Similarity tu Text - POST /similarity | |
| Nhan 2 doan van ban, chuyen ca 2 thanh vector embedding, | |
| roi tinh cosine similarity giua chung. | |
| Use case: So sanh truc tiep CV voi Job Description | |
| khi chua co vector luu san trong database. | |
| """ | |
| try: | |
| # Buoc 1: Chuyen 2 doan text thanh vector cung luc (batch) | |
| embeddings = generate_embeddings([request.text1, request.text2]) | |
| # Buoc 2: Tinh cosine similarity | |
| score = compute_cosine_similarity(embeddings[0], embeddings[1]) | |
| return SimilarityResponse( | |
| similarity_score=round(score, 6), | |
| percentage=round(max(0, score) * 100, 2), | |
| ) | |
| except Exception as e: | |
| traceback.print_exc() | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Error computing similarity: {str(e)}", | |
| ) | |
| async def compute_vector_similarity(request: SimilarityVectorRequest, api_key: str = Depends(get_api_key)): | |
| """ | |
| Endpoint Similarity tu Vector - POST /similarity/vectors | |
| Nhan 2 vector embedding da co san (lay tu database), | |
| tinh cosine similarity truc tiep ma KHONG can goi model. | |
| Use case: Backend da co san job_embedding va cv_embedding | |
| trong database, chi can gui len de tinh diem. | |
| Endpoint nay rat nhanh vi khong can chay model. | |
| """ | |
| # Validate: 2 vector phai cung so chieu | |
| if len(request.vector1) != len(request.vector2): | |
| raise HTTPException( | |
| status_code=400, | |
| detail=f"Vector dimensions do not match: {len(request.vector1)} vs {len(request.vector2)}", | |
| ) | |
| if len(request.vector1) == 0: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Vectors must not be empty", | |
| ) | |
| try: | |
| score = compute_cosine_similarity(request.vector1, request.vector2) | |
| return SimilarityResponse( | |
| similarity_score=round(score, 6), | |
| percentage=round(max(0, score) * 100, 2), | |
| ) | |
| except Exception as e: | |
| traceback.print_exc() | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Error computing similarity: {str(e)}", | |
| ) | |
| # ==================== MINILM ENDPOINTS ==================== | |
| async def create_embedding_minilm(request: EmbeddingRequest, api_key: str = Depends(get_api_key)): | |
| """ | |
| Endpoint Embedding MiniLM - POST /embed/minilm | |
| Nhan 1 hoac nhieu van ban, chuyen thanh vector 384 chieu | |
| bang model MiniLM. Dung cho behavior-based recommendation: | |
| - Embedding search query cua user | |
| - Embedding tieu de job da luu / da thich | |
| """ | |
| texts = request.get_texts() | |
| if not texts: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Please provide 'text' (string) or 'texts' (list[string])", | |
| ) | |
| try: | |
| embeddings = generate_embeddings_minilm(texts) | |
| except Exception as e: | |
| traceback.print_exc() | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Error generating MiniLM embedding: {str(e)}", | |
| ) | |
| return EmbeddingResponse( | |
| embeddings=embeddings, | |
| dimension=len(embeddings[0]) if embeddings else 0, | |
| num_texts=len(texts), | |
| ) | |
| async def compute_text_similarity_minilm(request: SimilarityTextRequest, api_key: str = Depends(get_api_key)): | |
| """ | |
| Endpoint Similarity MiniLM - POST /similarity/minilm | |
| So sanh 2 doan text ngan (search query, job title) | |
| bang model MiniLM. | |
| """ | |
| try: | |
| embeddings = generate_embeddings_minilm([request.text1, request.text2]) | |
| score = compute_cosine_similarity(embeddings[0], embeddings[1]) | |
| return SimilarityResponse( | |
| similarity_score=round(score, 6), | |
| percentage=round(max(0, score) * 100, 2), | |
| ) | |
| except Exception as e: | |
| traceback.print_exc() | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Error computing MiniLM similarity: {str(e)}", | |
| ) | |
| # ==================== CV EXTRACTION ==================== | |
| from fastapi import UploadFile, File | |
| from app.schemas import ExtractCVResponse, ExtractedCV | |
| from app.llm_service import extract_cv_info | |
| async def extract_cv_endpoint(file: UploadFile = File(...), api_key: str = Depends(get_api_key)): | |
| """ | |
| Endpoint xử lý CV - POST /extract-cv | |
| """ | |
| try: | |
| # Đọc nội dung file | |
| file_bytes = await file.read() | |
| # 1. Trích xuất thông tin JSON bằng LLM | |
| extracted_dict = extract_cv_info(file_bytes, file.filename) | |
| extracted_data = ExtractedCV(**extracted_dict) | |
| # 2. Trả về kết quả (Không tạo Vector ở bước này theo yêu cầu của team Backend) | |
| return ExtractCVResponse( | |
| status="ok", | |
| extracted_data=extracted_data, | |
| embedding=None, | |
| dimension=0, | |
| ) | |
| except Exception as e: | |
| traceback.print_exc() | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Error extracting CV: {str(e)}", | |
| ) | |
| # ==================== GRADio UI ==================== | |
| demo = create_gradio_interface() | |
| app = gr.mount_gradio_app(app, demo, path="/") | |