davanstrien HF Staff commited on
Commit
da95441
·
verified ·
1 Parent(s): 0ac08e4

Add /search_vector + OAuth-authed per-user feeds store

Browse files
Files changed (1) hide show
  1. app.py +98 -1
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}")