ai-matching-api / app /main.py
quocdat1964
Chỉnh api extract cv không tự động tạo embedding
0d0d8c0
Raw
History Blame Contribute Delete
11 kB
"""
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="/")