""" 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) ==================== @asynccontextmanager 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 ==================== @app.get( "/health", response_model=HealthResponse, summary="Health Check", description="Kiểm tra trạng thái hoạt động của API và model đang sử dụng", ) 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}", ) @app.post( "/embed", response_model=EmbeddingResponse, summary="Tạo Embedding", description="Nhận văn bản đầu vào và trả về vector nhúng (embedding vector)", ) 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), ) @app.post( "/similarity", response_model=SimilarityResponse, summary="So khop 2 doan text", description="Nhan 2 doan van ban, chuyen thanh vector roi tinh cosine similarity", ) 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)}", ) @app.post( "/similarity/vectors", response_model=SimilarityResponse, summary="So khop 2 vector co san", description="Nhan 2 vector embedding (tu database) roi tinh cosine similarity", ) 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 ==================== @app.post( "/embed/minilm", response_model=EmbeddingResponse, summary="Tao Embedding bang MiniLM", description="Nhan van ban va tra ve vector nhung 384 chieu tu MiniLM (behavior matching)", tags=["MiniLM"], ) 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), ) @app.post( "/similarity/minilm", response_model=SimilarityResponse, summary="So khop 2 text bang MiniLM", description="Nhan 2 doan van ban, chuyen thanh vector MiniLM roi tinh cosine similarity", tags=["MiniLM"], ) 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 @app.post( "/extract-cv", response_model=ExtractCVResponse, summary="Trích xuất thông tin CV (Chỉ JSON)", description="Nhận file CV PDF/Image, dùng AI trích xuất JSON. Endpoint này KHÔNG tự động tạo Vector.", tags=["CV Processing"], ) 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="/")