""" Hugging Science Feedback API A minimal FastAPI app that accepts feedback submissions and appends them to the hugging-science/feedback HF dataset. Deploy as a HF Space (Docker SDK): hugging-science/feedback-api Required Space secret: HF_TOKEN — a write-scoped token for the hugging-science org """ import os import json import uuid from datetime import datetime, timezone from typing import Optional from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, field_validator from huggingface_hub import HfApi, hf_hub_download import tempfile # ── Config ──────────────────────────────────────────────────────────────────── DATASET_REPO = "hugging-science/feedback" FEEDBACK_FILE = "feedback.jsonl" HF_TOKEN = os.environ.get("HF_TOKEN") if not HF_TOKEN: raise RuntimeError("HF_TOKEN secret is not set") api = HfApi(token=HF_TOKEN) # ── App ─────────────────────────────────────────────────────────────────────── app = FastAPI(title="Hugging Science Feedback API", version="1.0.0") app.add_middleware( CORSMiddleware, allow_origins=["https://huggingscience.co", "http://localhost:5173"], allow_methods=["POST", "GET"], allow_headers=["Content-Type"], ) # ── Schema ──────────────────────────────────────────────────────────────────── VALID_TYPES = {"dataset", "model", "challenge", "feedback"} class FeedbackItem(BaseModel): type: str title: Optional[str] = None description: str submitted_at: Optional[str] = None source: Optional[str] = "huggingscience.co" @field_validator("type") @classmethod def validate_type(cls, v): if v not in VALID_TYPES: raise ValueError(f"type must be one of {VALID_TYPES}") return v @field_validator("description") @classmethod def validate_description(cls, v): v = v.strip() if len(v) < 5: raise ValueError("description must be at least 5 characters") if len(v) > 2000: raise ValueError("description must be under 2000 characters") return v # ── Helpers ─────────────────────────────────────────────────────────────────── def load_existing() -> list[dict]: """Download the current feedback.jsonl from the dataset, return as list.""" try: path = hf_hub_download( repo_id=DATASET_REPO, filename=FEEDBACK_FILE, repo_type="dataset", token=HF_TOKEN, ) with open(path) as f: return [json.loads(line) for line in f if line.strip()] except Exception: # File doesn't exist yet — start fresh return [] def save_feedback(rows: list[dict]) -> None: """Upload the full feedback.jsonl back to the dataset.""" content = "\n".join(json.dumps(r, ensure_ascii=False) for r in rows) + "\n" with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f: f.write(content) tmp_path = f.name api.upload_file( path_or_fileobj=tmp_path, path_in_repo=FEEDBACK_FILE, repo_id=DATASET_REPO, repo_type="dataset", commit_message=f"Add feedback entry ({rows[-1]['id'][:8]})", ) # ── Routes ──────────────────────────────────────────────────────────────────── @app.get("/") def root(): return {"status": "ok", "service": "Hugging Science Feedback API"} @app.post("/submit", status_code=201) def submit_feedback(item: FeedbackItem): entry = { "id": str(uuid.uuid4()), "type": item.type, "title": item.title or "", "description": item.description, "submitted_at": item.submitted_at or datetime.now(timezone.utc).isoformat(), "source": item.source or "huggingscience.co", "status": "pending", } try: rows = load_existing() rows.append(entry) save_feedback(rows) except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to save feedback: {e}") return {"ok": True, "id": entry["id"]}