Spaces:
Running
Running
Add /search_vector + OAuth-authed per-user feeds store
Browse files
app.py
CHANGED
|
@@ -17,7 +17,7 @@ from typing import List, Optional
|
|
| 17 |
import duckdb
|
| 18 |
import httpx
|
| 19 |
from cashews import cache
|
| 20 |
-
from fastapi import FastAPI, HTTPException, Query
|
| 21 |
from fastapi.middleware.cors import CORSMiddleware
|
| 22 |
from huggingface_hub import HfApi, hf_hub_download, list_repo_files, login
|
| 23 |
from pydantic import BaseModel
|
|
@@ -591,6 +591,103 @@ async def embed_query_route(text: str = Query(min_length=3, max_length=200)):
|
|
| 591 |
return {"embedding": embed_query(f"Instruct: {DS_TASK}\nQuery:{text}")}
|
| 592 |
|
| 593 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
# --- suggest (autocomplete; old backend never implemented it — bonus) --------
|
| 595 |
@app.get("/suggest/{repo_type}")
|
| 596 |
@cache(ttl="1h", key="suggest:{repo_type}:{q}")
|
|
|
|
| 17 |
import duckdb
|
| 18 |
import httpx
|
| 19 |
from cashews import cache
|
| 20 |
+
from fastapi import FastAPI, Header, HTTPException, Query
|
| 21 |
from fastapi.middleware.cors import CORSMiddleware
|
| 22 |
from huggingface_hub import HfApi, hf_hub_download, list_repo_files, login
|
| 23 |
from pydantic import BaseModel
|
|
|
|
| 591 |
return {"embedding": embed_query(f"Instruct: {DS_TASK}\nQuery:{text}")}
|
| 592 |
|
| 593 |
|
| 594 |
+
class VectorSearchReq(BaseModel):
|
| 595 |
+
repo_type: str
|
| 596 |
+
vector: List[float]
|
| 597 |
+
k: int = 20
|
| 598 |
+
exclude_ids: List[str] = []
|
| 599 |
+
include_embeddings: bool = False
|
| 600 |
+
|
| 601 |
+
|
| 602 |
+
@app.post("/search_vector")
|
| 603 |
+
async def search_vector(req: VectorSearchReq):
|
| 604 |
+
"""KNN search with a caller-supplied (already 256-d, normalised) vector.
|
| 605 |
+
|
| 606 |
+
Powers relevance-feedback loops: refine a preference vector client-side
|
| 607 |
+
from votes, then pull fresh results for it. exclude_ids drops already-seen
|
| 608 |
+
items server-side so refinement surfaces new material.
|
| 609 |
+
"""
|
| 610 |
+
if req.repo_type not in ("datasets", "models"):
|
| 611 |
+
raise HTTPException(status_code=422, detail="repo_type must be datasets|models")
|
| 612 |
+
if len(req.vector) != EMB_DIM:
|
| 613 |
+
raise HTTPException(status_code=422, detail=f"vector must be {EMB_DIM}-d")
|
| 614 |
+
table = "dataset_cards" if req.repo_type == "datasets" else "model_cards"
|
| 615 |
+
k = max(1, min(int(req.k), 100))
|
| 616 |
+
n = k + len(req.exclude_ids[:300])
|
| 617 |
+
raw = _knn(table, req.vector, n, "", COLS + (", emb" if req.include_embeddings else ""))
|
| 618 |
+
excl = set(req.exclude_ids[:300])
|
| 619 |
+
out = []
|
| 620 |
+
for row in raw:
|
| 621 |
+
emb_val = row[-2] if req.include_embeddings else None
|
| 622 |
+
(id_, summary, likes, downloads, param, task, license_, language, last_modified) = row[:9]
|
| 623 |
+
if id_ in excl:
|
| 624 |
+
continue
|
| 625 |
+
item = {"id": id_, "summary": summary, "likes": int(likes),
|
| 626 |
+
"downloads": int(downloads), "param_count": int(param),
|
| 627 |
+
"task": task, "license": license_, "language": language,
|
| 628 |
+
"last_modified": last_modified, "similarity": float(row[-1])}
|
| 629 |
+
if req.include_embeddings:
|
| 630 |
+
item["embedding"] = [float(x) for x in emb_val]
|
| 631 |
+
out.append(item)
|
| 632 |
+
if len(out) >= k:
|
| 633 |
+
break
|
| 634 |
+
return {"results": out}
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
# --- per-user feeds store (OAuth token -> whoami -> one JSON doc per user) ---
|
| 638 |
+
FEEDS_STORE_REPO = os.getenv("FEEDS_STORE_REPO", "davanstrien/hub-feeds-store")
|
| 639 |
+
FEEDS_DOC_LIMIT = 300_000 # bytes
|
| 640 |
+
|
| 641 |
+
|
| 642 |
+
@cache(ttl="10m", key="whoami:{token}")
|
| 643 |
+
async def _resolve_user(token: str) -> str:
|
| 644 |
+
async with httpx.AsyncClient(timeout=10) as c:
|
| 645 |
+
r = await c.get("https://huggingface.co/api/whoami-v2",
|
| 646 |
+
headers={"Authorization": f"Bearer {token}"})
|
| 647 |
+
if r.status_code != 200:
|
| 648 |
+
raise HTTPException(status_code=401, detail="Invalid or expired HF token")
|
| 649 |
+
name = r.json().get("name")
|
| 650 |
+
if not name:
|
| 651 |
+
raise HTTPException(status_code=401, detail="Could not resolve username")
|
| 652 |
+
return name
|
| 653 |
+
|
| 654 |
+
|
| 655 |
+
async def _auth_user(authorization: str) -> str:
|
| 656 |
+
if not authorization or not authorization.startswith("Bearer "):
|
| 657 |
+
raise HTTPException(status_code=401, detail="Missing Bearer token")
|
| 658 |
+
return await _resolve_user(authorization[7:])
|
| 659 |
+
|
| 660 |
+
|
| 661 |
+
@app.get("/feeds_store")
|
| 662 |
+
async def feeds_load(authorization: str = Header(default=None)):
|
| 663 |
+
user = await _auth_user(authorization)
|
| 664 |
+
import json as _json
|
| 665 |
+
try:
|
| 666 |
+
path = hf_hub_download(FEEDS_STORE_REPO, f"users/{user}.json",
|
| 667 |
+
repo_type="dataset", force_download=True)
|
| 668 |
+
with open(path) as f:
|
| 669 |
+
return {"user": user, "doc": _json.load(f)}
|
| 670 |
+
except Exception:
|
| 671 |
+
return {"user": user, "doc": None}
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
class FeedsSaveReq(BaseModel):
|
| 675 |
+
doc: dict
|
| 676 |
+
|
| 677 |
+
|
| 678 |
+
@app.put("/feeds_store")
|
| 679 |
+
async def feeds_save(req: FeedsSaveReq, authorization: str = Header(default=None)):
|
| 680 |
+
user = await _auth_user(authorization)
|
| 681 |
+
import json as _json
|
| 682 |
+
payload = _json.dumps(req.doc).encode()
|
| 683 |
+
if len(payload) > FEEDS_DOC_LIMIT:
|
| 684 |
+
raise HTTPException(status_code=413, detail="Feeds doc too large")
|
| 685 |
+
HfApi().upload_file(path_or_fileobj=payload, path_in_repo=f"users/{user}.json",
|
| 686 |
+
repo_id=FEEDS_STORE_REPO, repo_type="dataset",
|
| 687 |
+
commit_message=f"feeds sync: {user}")
|
| 688 |
+
return {"ok": True, "user": user, "bytes": len(payload)}
|
| 689 |
+
|
| 690 |
+
|
| 691 |
# --- suggest (autocomplete; old backend never implemented it — bonus) --------
|
| 692 |
@app.get("/suggest/{repo_type}")
|
| 693 |
@cache(ttl="1h", key="suggest:{repo_type}:{q}")
|