Emma Scharfmann commited on
Commit
7a7515d
Β·
1 Parent(s): d3063bb
Files changed (5) hide show
  1. .gitignore +1 -0
  2. Dockerfile +12 -0
  3. README.md +17 -12
  4. app.py +145 -0
  5. requirements.txt +5 -0
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ .env
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY main.py .
9
+
10
+ EXPOSE 7860
11
+
12
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,12 +1,17 @@
1
- ---
2
- title: Request Api
3
- emoji: πŸ“Š
4
- colorFrom: purple
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- license: apache-2.0
9
- short_description: request api for huggingscience website
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
1
+ # Hugging Science Feedback API
2
+
3
+ Source for the live `hugging-science/feedback-api` Space (Docker SDK), the
4
+ backend `src/components/FeedbackWidget.jsx` POSTs to.
5
+
6
+ Only change from the previously deployed version: `type: "feedback"`
7
+ submissions still go to `feedback.jsonl`, but every content-addition type
8
+ (`dataset`, `model`, `organization`, `blog`, `challenge`) now goes to a new
9
+ `requests.jsonl` file instead β€” both in the `hugging-science/request`
10
+ dataset. The `hugging-science/requests-review` Space (see `../moderation-space/`)
11
+ reads `requests.jsonl` to approve/reject those.
12
+
13
+ This directory only contains `app.py` β€” merge it into the existing Space
14
+ repo's deployment (same `Dockerfile`/`requirements.txt` you already have;
15
+ `requirements.txt` here is for reference, no new dependencies were added).
16
+
17
+ Required Space secret: `HF_TOKEN` (write-scoped, org `hugging-science`) β€” unchanged.
app.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Science Feedback API
3
+ A minimal FastAPI app that accepts feedback and content-addition submissions.
4
+
5
+ `type: "feedback"` submissions go to feedback.jsonl (unchanged behavior).
6
+ Every other type (dataset/model/organization/blog/challenge) is a request to
7
+ add something to the site, and goes to requests.jsonl instead β€” the
8
+ hugging-science/requests-review Space reads that file to approve/reject them.
9
+ Both files live in the same hugging-science/feedback dataset.
10
+
11
+ Deploy as a HF Space (Docker SDK):
12
+ hugging-science/feedback-api
13
+ Required Space secret:
14
+ HF_TOKEN β€” a write-scoped token for the hugging-science org
15
+ """
16
+
17
+ import os
18
+ import json
19
+ import uuid
20
+ from datetime import datetime, timezone
21
+ from typing import Optional
22
+
23
+ from fastapi import FastAPI, HTTPException
24
+ from fastapi.middleware.cors import CORSMiddleware
25
+ from pydantic import BaseModel, field_validator
26
+ from huggingface_hub import HfApi, hf_hub_download
27
+ import tempfile
28
+ from dotenv import load_dotenv
29
+
30
+ load_dotenv()
31
+
32
+ # ── Config ────────────────────────────────────────────────────────────────────
33
+
34
+ DATASET_REPO = "hugging-science/request"
35
+ FEEDBACK_FILE = "feedback.jsonl"
36
+ REQUESTS_FILE = "requests.jsonl"
37
+
38
+ HF_TOKEN = os.environ.get("HF_TOKEN")
39
+ if not HF_TOKEN:
40
+ raise RuntimeError("HF_TOKEN secret is not set")
41
+
42
+ api = HfApi(token=HF_TOKEN)
43
+
44
+ # ── App ───────────────────────────────────────────────────────────────────────
45
+
46
+ app = FastAPI(title="Hugging Science Feedback API", version="1.1.0")
47
+
48
+ app.add_middleware(
49
+ CORSMiddleware,
50
+ allow_origins=["https://huggingscience.co", "http://localhost:5173"],
51
+ allow_methods=["POST", "GET"],
52
+ allow_headers=["Content-Type"],
53
+ )
54
+
55
+ # ── Schema ────────────────────────────────────────────────────────────────────
56
+
57
+ VALID_TYPES = {"dataset", "model", "organization", "blog", "challenge", "feedback"}
58
+ REQUEST_TYPES = VALID_TYPES - {"feedback"}
59
+
60
+ class FeedbackItem(BaseModel):
61
+ type: str
62
+ title: Optional[str] = None
63
+ description: str
64
+ submitted_at: Optional[str] = None
65
+ source: Optional[str] = "huggingscience.co"
66
+
67
+ @field_validator("type")
68
+ @classmethod
69
+ def validate_type(cls, v):
70
+ if v not in VALID_TYPES:
71
+ raise ValueError(f"type must be one of {VALID_TYPES}")
72
+ return v
73
+
74
+ @field_validator("description")
75
+ @classmethod
76
+ def validate_description(cls, v):
77
+ v = v.strip()
78
+ if len(v) < 5:
79
+ raise ValueError("description must be at least 5 characters")
80
+ if len(v) > 2000:
81
+ raise ValueError("description must be under 2000 characters")
82
+ return v
83
+
84
+ # ── Helpers ───────────────────────────────────────────────────────────────────
85
+
86
+ def load_existing(filename: str) -> list[dict]:
87
+ """Download the given jsonl file from the dataset, return as a list."""
88
+ try:
89
+ path = hf_hub_download(
90
+ repo_id=DATASET_REPO,
91
+ filename=filename,
92
+ repo_type="dataset",
93
+ token=HF_TOKEN,
94
+ )
95
+ with open(path) as f:
96
+ return [json.loads(line) for line in f if line.strip()]
97
+ except Exception:
98
+ # File doesn't exist yet β€” start fresh
99
+ return []
100
+
101
+
102
+ def save_rows(filename: str, rows: list[dict]) -> None:
103
+ """Upload the full jsonl file back to the dataset."""
104
+ content = "\n".join(json.dumps(r, ensure_ascii=False) for r in rows) + "\n"
105
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f:
106
+ f.write(content)
107
+ tmp_path = f.name
108
+
109
+ api.upload_file(
110
+ path_or_fileobj=tmp_path,
111
+ path_in_repo=filename,
112
+ repo_id=DATASET_REPO,
113
+ repo_type="dataset",
114
+ commit_message=f"Add entry ({rows[-1]['id'][:8]}) to {filename}",
115
+ )
116
+
117
+ # ── Routes ────────────────────────────────────────────────────────────────────
118
+
119
+ @app.get("/")
120
+ def root():
121
+ return {"status": "ok", "service": "Hugging Science Feedback API"}
122
+
123
+
124
+ @app.post("/submit", status_code=201)
125
+ def submit_feedback(item: FeedbackItem):
126
+ entry = {
127
+ "id": str(uuid.uuid4()),
128
+ "type": item.type,
129
+ "title": item.title or "",
130
+ "description": item.description,
131
+ "submitted_at": item.submitted_at or datetime.now(timezone.utc).isoformat(),
132
+ "source": item.source or "huggingscience.co",
133
+ "status": "pending",
134
+ }
135
+
136
+ target_file = REQUESTS_FILE if item.type in REQUEST_TYPES else FEEDBACK_FILE
137
+
138
+ try:
139
+ rows = load_existing(target_file)
140
+ rows.append(entry)
141
+ save_rows(target_file, rows)
142
+ except Exception as e:
143
+ raise HTTPException(status_code=500, detail=f"Failed to save {item.type}: {e}")
144
+
145
+ return {"ok": True, "id": entry["id"]}
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi>=0.110.0
2
+ uvicorn[standard]>=0.29.0
3
+ pydantic>=2.6.0
4
+ huggingface_hub>=0.25.0
5
+ dotenv