namboqa-api / app.py
phamluan's picture
NamBoQA API v2 (dữ liệu vòng 3)
7337389 verified
Raw History Blame Contribute Delete
4.39 kB
# -*- coding: utf-8 -*-
"""
API demo NamBoQA — chạy trên Hugging Face Spaces (Docker, cổng 7860).
GET /health trạng thái từng thành phần (BM25, bi-encoder, reader...)
GET /stats thống kê bộ dữ liệu + mô hình đang dùng
GET /samples câu hỏi mẫu (mặc định lấy từ tập test)
POST /ask hỏi: truy hồi top-k rồi đọc hiểu
Chạy local:
pip install -r requirements.txt
uvicorn app:app --reload --port 7860
"""
from __future__ import annotations
import logging
import os
import threading
import time
from collections import OrderedDict, defaultdict, deque
from typing import Literal, Optional
from fastapi import Depends, FastAPI, Header, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from engine import Engine
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s", datefmt="%H:%M:%S")
log = logging.getLogger("namboqa")
engine = Engine(os.environ.get("DATA_DIR", "data"))
app = FastAPI(title="NamBoQA demo API", version="1.0",
description="Hỏi–đáp du lịch và di sản văn hoá Nam Bộ (RAG hai giai đoạn)")
origins = [o.strip() for o in os.environ.get("ALLOWED_ORIGINS", "*").split(",") if o.strip()]
app.add_middleware(CORSMiddleware, allow_origins=origins, allow_methods=["GET", "POST"],
allow_headers=["*"])
@app.on_event("startup")
def _startup():
engine.load_fast() # dữ liệu + BM25: vài giây
threading.Thread(target=engine.load_heavy, daemon=True).start() # mô hình: chạy nền
# ------------------------------------------------------------------ bảo vệ
API_KEY = os.environ.get("DEMO_API_KEY") # đặt thì mọi request phải gửi X-API-Key
RATE = int(os.environ.get("RATE_LIMIT_PER_MIN", "20"))
_hits: dict[str, deque] = defaultdict(deque)
def guard(request: Request, x_api_key: Optional[str] = Header(default=None)):
if API_KEY and x_api_key != API_KEY:
raise HTTPException(401, "Sai hoặc thiếu X-API-Key")
# proxy trên Vercel chuyển IP người dùng qua x-client-ip; gọi thẳng thì dùng x-forwarded-for
ip = (request.headers.get("x-client-ip") or request.headers.get("x-forwarded-for")
or (request.client.host if request.client else "?"))
ip = ip.split(",")[0].strip()
now, q = time.time(), _hits[ip]
while q and now - q[0] > 60:
q.popleft()
if len(q) >= RATE:
raise HTTPException(429, f"Quá {RATE} câu hỏi/phút, thử lại sau ít giây")
q.append(now)
# ------------------------------------------------------------------ cache
_cache: OrderedDict = OrderedDict()
CACHE_SIZE = 256
class AskIn(BaseModel):
question: str = Field(..., min_length=3, max_length=500)
retriever: Literal["bm25", "dense", "hybrid"] = "hybrid"
reader: Literal["extractive", "generative", "none"] = "generative"
top_k: int = Field(5, ge=1, le=10)
alpha: Optional[float] = Field(None, ge=0.0, le=1.0)
@app.get("/")
def root():
return {"name": "NamBoQA demo API", "docs": "/docs", "health": "/health"}
@app.get("/health")
def health():
return {"status": engine.status, "errors": engine.errors,
"ready": engine.status["bm25"] == "ready"}
@app.get("/stats")
def stats():
return engine.stats()
@app.get("/samples")
def samples(n: int = 6, type: Optional[str] = None, split: str = "test", seed: Optional[int] = None):
return engine.samples(min(n, 30), type, split, seed)
@app.post("/ask", dependencies=[Depends(guard)])
def ask(body: AskIn):
key = (body.question.strip(), body.retriever, body.reader, body.top_k, body.alpha,
engine.status["dense"], engine.status["extractive"])
if key in _cache:
_cache.move_to_end(key)
return {**_cache[key], "cached": True}
try:
res = engine.ask(body.question.strip(), body.retriever, body.reader, body.top_k, body.alpha)
except RuntimeError as e:
raise HTTPException(503, str(e))
except Exception as e:
log.exception("lỗi khi trả lời")
raise HTTPException(500, f"Lỗi back-end: {str(e)[:200]}")
_cache[key] = res
if len(_cache) > CACHE_SIZE:
_cache.popitem(last=False)
return res