File size: 5,401 Bytes
44d743a 15d54db 44d743a 15d54db 17ee6fb 44d743a 6cfa7b8 44d743a 6cfa7b8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | """Decoding Parliament β HTTP API (FastAPI). Runs on a Hugging Face Space (Docker SDK, free CPU) or locally.
uvicorn server:app --port 7860 # local
POST /ask {"question": "...", "k": 5} β {"answer", "model", "retriever", "passages": [{n, citation, text, speech_id, score}], "seconds"}
GET /health β {"status": "ok", "passages": 158945, ...}
Configuration by environment: GEMINI_API_KEY (required; a Space secret), DP_EMBED (sealion), DP_MODE (hybrid),
DP_PROVIDER (gemini), DP_MODEL, DP_ORIGINS (comma-separated allowed origins; default *), DP_RATE (requests per hour per IP, default 30).
Nothing is logged except request counts for rate limiting (in memory, per IP, reset hourly).
"""
import os, time, threading
from collections import defaultdict, deque
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
EMBED = os.environ.get("DP_EMBED", "sealion"); MODE = os.environ.get("DP_MODE", "hybrid")
PROVIDER = os.environ.get("DP_PROVIDER", "gemini"); MODEL = os.environ.get("DP_MODEL") or None
ORIGINS = [o.strip() for o in os.environ.get("DP_ORIGINS", "*").split(",")]
RATE = int(os.environ.get("DP_RATE", "30"))
K_MAX, Q_MAX = 8, 400
app = FastAPI(title="Decoding Parliament API", version="1.0")
app.add_middleware(CORSMiddleware, allow_origins=ORIGINS, allow_methods=["POST", "GET"], allow_headers=["*"])
STATE = {"ret": None, "gen": None, "error": None}
_hits = defaultdict(deque); _lock = threading.Lock()
def load():
try:
from retrieval import Retriever
from rag import Generator
STATE["ret"] = Retriever(model=EMBED, mode=MODE)
STATE["gen"] = Generator(PROVIDER, MODEL)
except SystemExit as e:
STATE["error"] = f"generator not configured: {e}"
except Exception as e:
STATE["error"] = f"load failed: {e}"
@app.on_event("startup")
def _startup():
threading.Thread(target=load, daemon=True).start() # answer /health while the index loads
def rate_ok(ip):
now = time.time()
with _lock:
q = _hits[ip]
while q and q[0] < now - 3600: q.popleft()
if len(q) >= RATE: return False
q.append(now); return True
class Ask(BaseModel):
question: str = Field(..., min_length=2, max_length=Q_MAX)
k: int = Field(5, ge=1, le=K_MAX)
lang: str = Field("auto", pattern="^(auto|my|en)$") # answer language: question's language, Burmese, or English
@app.get("/health")
def health():
r = STATE["ret"]
return {"status": "ok" if r else ("error" if STATE["error"] else "loading"), "error": STATE["error"],
"passages": len(r.meta) if r else None, "retriever": r.name if r else None,
"generator": f"{STATE['gen'].provider}/{STATE['gen'].model}" if STATE["gen"] else None}
@app.post("/ask")
def ask(body: Ask, request: Request):
if STATE["error"]: raise HTTPException(503, STATE["error"])
if not STATE["ret"]: raise HTTPException(503, "index still loading β try again in a minute")
ip = request.headers.get("x-forwarded-for", request.client.host if request.client else "?").split(",")[0].strip()
if not rate_ok(ip): raise HTTPException(429, f"rate limit: {RATE} questions per hour")
from rag import answer
t = time.time()
res = answer(body.question.strip(), STATE["ret"], STATE["gen"], k=body.k, per_speech=2, lang=None if body.lang == "auto" else body.lang)
return {"question": res["question"], "answer": res["answer"], "model": res["model"], "retriever": res["retriever"], "lang": res["lang"], "query_my": res.get("query_my"),
"passages": [{"n": p["n"], "citation": p["citation"], "text": p["text"], "speech_id": p["speech_id"], "score": p["score"]} for p in res["passages"]],
"seconds": round(time.time() - t, 2)}
class Search(BaseModel):
question: str = Field(..., min_length=2, max_length=Q_MAX)
k: int = Field(5, ge=1, le=K_MAX)
mode: str = Field("bm25", pattern="^(bm25|dense|hybrid)$")
@app.post("/search")
def search(body: Search, request: Request):
"""Passages only, no generated answer. mode=bm25 is the SQ3 comparison condition ("keyword search over the archive")."""
if not STATE["ret"]: raise HTTPException(503, "index still loading β try again in a minute")
ip = request.headers.get("x-forwarded-for", request.client.host if request.client else "?").split(",")[0].strip()
if not rate_ok(ip): raise HTTPException(429, f"rate limit: {RATE} questions per hour")
from retrieval import Retriever
r = STATE["ret"]
ret = r if body.mode == r.mode else Retriever(model=r.model_key, mode=body.mode, meta=r.meta, dense=r.dense, bm25=r.bm25, bm25_weight=r.bm25_weight)
from common import citation_of
t = time.time(); hits = ret.search(body.question.strip(), k=body.k, per_speech=2)
return {"question": body.question, "mode": body.mode, "retriever": ret.name,
"passages": [{"n": i, "citation": citation_of(m), "text": m["text"], "speech_id": m["speech_id"], "score": round(float(sc), 4)} for i, (sc, m) in enumerate(hits, 1)],
"seconds": round(time.time() - t, 2)}
@app.get("/")
def root():
return {"name": "Decoding Parliament API", "docs": "/docs", "health": "/health", "ask": "POST /ask {question, k, lang}", "search": "POST /search {question, k, mode}"}
|