vlps-demo / server.py
ORCMS's picture
Per-session agent memory + new-chat reset
69f85e5
Raw
History Blame Contribute Delete
4.3 kB
"""
FastAPI backend for the VLPS custom website.
Serves the static frontend (web/) and a JSON API over the core logic.
Run locally: uvicorn server:app --reload --port 7860
On HF Spaces: CMD in Dockerfile runs uvicorn on 0.0.0.0:7860
"""
import base64
import io
import os
import secrets
from fastapi import FastAPI, UploadFile, File, Form, Request
from fastapi.responses import JSONResponse, StreamingResponse, Response
from fastapi.staticfiles import StaticFiles
from PIL import Image
import core
app = FastAPI(title="VLPS Demo")
# Optional password gate. Set APP_PASSWORD (and optionally APP_USER) as a Space
# secret to require a login on a public Space. Left unset -> open (local dev).
APP_USER = os.getenv("APP_USER", "grader")
APP_PASSWORD = os.getenv("APP_PASSWORD", "")
@app.middleware("http")
async def password_gate(request: Request, call_next):
if APP_PASSWORD:
header = request.headers.get("authorization", "")
ok = False
if header.startswith("Basic "):
try:
user, _, pwd = base64.b64decode(header[6:]).decode().partition(":")
ok = (secrets.compare_digest(user, APP_USER)
and secrets.compare_digest(pwd, APP_PASSWORD))
except Exception: # noqa: BLE001
ok = False
if not ok:
return Response(status_code=401, headers={"WWW-Authenticate": 'Basic realm="VLPS"'})
response = await call_next(request)
# Always revalidate static assets so UI updates show without a hard refresh.
response.headers["Cache-Control"] = "no-cache"
return response
def _pil(upload: UploadFile) -> Image.Image:
return Image.open(io.BytesIO(upload.file.read())).convert("RGB")
def _ok(data):
return JSONResponse(data)
def _err(e):
return JSONResponse({"error": str(e)}, status_code=200)
@app.get("/api/config")
def config():
return {"model": core.MODEL, "user": core.USER}
@app.post("/api/search")
async def api_search(payload: dict):
try:
return _ok({"results": core.search_images(
payload.get("query", ""),
payload.get("mode", "knn"),
int(payload.get("top_k", 6)),
)})
except Exception as e: # noqa: BLE001
return _err(e)
@app.post("/api/vqa")
async def api_vqa(image: UploadFile = File(...), question: str = Form(...),
system_prompt: str = Form("")):
try:
return _ok({"answer": core.visual_qa(_pil(image), question, system_prompt)})
except Exception as e: # noqa: BLE001
return _err(e)
@app.post("/api/ocr_qa")
async def api_ocr_qa(image: UploadFile = File(...), question: str = Form(...),
lang: str = Form("en")):
try:
return _ok(core.ocr_qa(_pil(image), question, lang))
except Exception as e: # noqa: BLE001
return _err(e)
@app.get("/api/dataset")
async def api_dataset(dataset: str = "textvqa", limit: int = 12):
try:
return _ok({"items": core.dataset_browse(dataset, limit)})
except Exception as e: # noqa: BLE001
return _err(e)
@app.post("/api/dataset/search")
async def api_dataset_search(payload: dict):
try:
return _ok({"items": core.dataset_search(
payload.get("dataset", "textvqa"),
payload.get("query", ""),
int(payload.get("k", 6)),
)})
except Exception as e: # noqa: BLE001
return _err(e)
@app.post("/api/agent")
async def api_agent(payload: dict):
try:
return _ok(core.agent_answer(payload.get("message", ""), payload.get("session", "")))
except Exception as e: # noqa: BLE001
return _err(e)
@app.post("/api/agent/reset")
async def api_agent_reset(payload: dict):
core.reset_agent(payload.get("session", ""))
return _ok({"ok": True})
@app.get("/pt-image/{image_id}")
def pt_image(image_id: str):
_, lookup = core.pt_data()
img = lookup.get(image_id)
if img is None:
return JSONResponse({"error": "not found"}, status_code=404)
buf = io.BytesIO()
img.save(buf, format="JPEG")
buf.seek(0)
return StreamingResponse(buf, media_type="image/jpeg")
# Static frontend (mounted last so /api/* take precedence).
app.mount("/", StaticFiles(directory="web", html=True), name="web")