Download server.py from standinothegr8/decoding-parliament-api: direct link, hf CLI and curl.
- Browser
- Download file 5.4 kB
-
https://huggingface.co/spaces/standinothegr8/decoding-parliament-api/resolve/main/server.py
- Command line
-
hf download hf://spaces/standinothegr8/decoding-parliament-api/server.py
-
curl -L -o server.py https://huggingface.co/spaces/standinothegr8/decoding-parliament-api/resolve/main/server.py
5.4 kB
| """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}" | |
| 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 | |
| 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} | |
| 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)$") | |
| 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)} | |
| def root(): | |
| return {"name": "Decoding Parliament API", "docs": "/docs", "health": "/health", "ask": "POST /ask {question, k, lang}", "search": "POST /search {question, k, mode}"} | |