Spaces:
Sleeping
Sleeping
Commit Β·
02a536f
1
Parent(s): 13525d6
Accept structured fields so reviewers can approve with one click
Browse filesSubmissions for dataset/model/organization/blog now carry slug, name,
org_id, entry_type, tags, link and date directly, and this derives the
remaining fields (entry_id, org_id, link, blog slug) that a submitter
shouldn't have to type by hand.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
app.py
CHANGED
|
@@ -10,6 +10,12 @@ hugging-science/requests-review Space reads that file to approve/reject them.
|
|
| 10 |
are acknowledged rather than turned into a PR (see that Space's PR_TYPES).
|
| 11 |
Both files live in the same hugging-science/feedback dataset.
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
Deploy as a HF Space (Docker SDK):
|
| 14 |
hugging-science/feedback-api
|
| 15 |
Required Space secret:
|
|
@@ -59,7 +65,18 @@ app.add_middleware(
|
|
| 59 |
|
| 60 |
VALID_TYPES = {"dataset", "model", "organization", "blog", "challenge", "collaboration", "feedback"}
|
| 61 |
REQUEST_TYPES = VALID_TYPES - {"feedback"}
|
|
|
|
| 62 |
EMAIL_RE = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
class FeedbackItem(BaseModel):
|
| 65 |
type: str
|
|
@@ -70,6 +87,16 @@ class FeedbackItem(BaseModel):
|
|
| 70 |
submitted_at: Optional[str] = None
|
| 71 |
source: Optional[str] = "huggingscience.co"
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
@field_validator("type")
|
| 74 |
@classmethod
|
| 75 |
def validate_type(cls, v):
|
|
@@ -100,8 +127,97 @@ class FeedbackItem(BaseModel):
|
|
| 100 |
self.institution = institution
|
| 101 |
return self
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
# ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
def load_existing(filename: str) -> list[dict]:
|
| 106 |
"""Download the given jsonl file from the dataset, return as a list."""
|
| 107 |
try:
|
|
@@ -154,6 +270,7 @@ def submit_feedback(item: FeedbackItem):
|
|
| 154 |
if item.type == "collaboration":
|
| 155 |
entry["email"] = item.email
|
| 156 |
entry["institution"] = item.institution
|
|
|
|
| 157 |
|
| 158 |
target_file = REQUESTS_FILE if item.type in REQUEST_TYPES else FEEDBACK_FILE
|
| 159 |
|
|
|
|
| 10 |
are acknowledged rather than turned into a PR (see that Space's PR_TYPES).
|
| 11 |
Both files live in the same hugging-science/feedback dataset.
|
| 12 |
|
| 13 |
+
`dataset`/`model`/`organization`/`blog` requests carry the full structured
|
| 14 |
+
fields the moderator Space needs to render a src/data/*.js entry (slug,
|
| 15 |
+
org_id, entry_type, tags, ...) so the reviewer only has to click Approve β
|
| 16 |
+
this module derives the remaining fields (entry_id, org_id, link, blog slug)
|
| 17 |
+
that submitters shouldn't have to type by hand.
|
| 18 |
+
|
| 19 |
Deploy as a HF Space (Docker SDK):
|
| 20 |
hugging-science/feedback-api
|
| 21 |
Required Space secret:
|
|
|
|
| 65 |
|
| 66 |
VALID_TYPES = {"dataset", "model", "organization", "blog", "challenge", "collaboration", "feedback"}
|
| 67 |
REQUEST_TYPES = VALID_TYPES - {"feedback"}
|
| 68 |
+
PR_TYPES = {"dataset", "model", "organization", "blog"}
|
| 69 |
EMAIL_RE = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")
|
| 70 |
+
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
| 71 |
+
|
| 72 |
+
# Mirrors formatters.VALID_TAGS / src/data/themes.js themeIds in the other
|
| 73 |
+
# two repos β kept in sync by hand since each Space deploys independently.
|
| 74 |
+
VALID_TAGS = {
|
| 75 |
+
"biology", "chemistry", "physics", "medicine", "mathematics",
|
| 76 |
+
"engineering", "earth-science", "astronomy", "genomics",
|
| 77 |
+
"biotechnology", "materials-science", "climate", "energy",
|
| 78 |
+
"ecology", "conservation", "benchmark", "scientific-reasoning",
|
| 79 |
+
}
|
| 80 |
|
| 81 |
class FeedbackItem(BaseModel):
|
| 82 |
type: str
|
|
|
|
| 87 |
submitted_at: Optional[str] = None
|
| 88 |
source: Optional[str] = "huggingscience.co"
|
| 89 |
|
| 90 |
+
# dataset / model / organization / blog β structured fields so the
|
| 91 |
+
# moderator Space can open a PR without the reviewer typing anything.
|
| 92 |
+
slug: Optional[str] = None
|
| 93 |
+
name: Optional[str] = None
|
| 94 |
+
org_id: Optional[str] = None
|
| 95 |
+
entry_type: Optional[str] = None
|
| 96 |
+
link: Optional[str] = None
|
| 97 |
+
date: Optional[str] = None
|
| 98 |
+
tags: Optional[list[str]] = None
|
| 99 |
+
|
| 100 |
@field_validator("type")
|
| 101 |
@classmethod
|
| 102 |
def validate_type(cls, v):
|
|
|
|
| 127 |
self.institution = institution
|
| 128 |
return self
|
| 129 |
|
| 130 |
+
@model_validator(mode="after")
|
| 131 |
+
def validate_pr_fields(self):
|
| 132 |
+
if self.type not in PR_TYPES:
|
| 133 |
+
return self
|
| 134 |
+
|
| 135 |
+
tags = [t.strip() for t in (self.tags or []) if t and t.strip()]
|
| 136 |
+
if not tags:
|
| 137 |
+
raise ValueError(f"at least one tag is required for {self.type} requests")
|
| 138 |
+
invalid = sorted(set(tags) - VALID_TAGS)
|
| 139 |
+
if invalid:
|
| 140 |
+
raise ValueError(f"invalid tags: {invalid}")
|
| 141 |
+
self.tags = tags
|
| 142 |
+
|
| 143 |
+
if self.type == "organization":
|
| 144 |
+
self.org_id = (self.org_id or "").strip()
|
| 145 |
+
self.name = (self.name or "").strip()
|
| 146 |
+
if not self.org_id:
|
| 147 |
+
raise ValueError("org_id is required for organization requests")
|
| 148 |
+
if not self.name:
|
| 149 |
+
raise ValueError("name is required for organization requests")
|
| 150 |
+
|
| 151 |
+
if self.type in {"model", "dataset"}:
|
| 152 |
+
self.slug = (self.slug or "").strip()
|
| 153 |
+
parts = self.slug.split("/")
|
| 154 |
+
if len(parts) != 2 or not parts[0] or not parts[1]:
|
| 155 |
+
raise ValueError(f"slug must be an 'org/repo' path for {self.type} requests")
|
| 156 |
+
self.entry_type = (self.entry_type or "").strip()
|
| 157 |
+
if not self.entry_type:
|
| 158 |
+
raise ValueError(f"entry_type is required for {self.type} requests")
|
| 159 |
+
if self.type == "model":
|
| 160 |
+
self.name = (self.name or "").strip()
|
| 161 |
+
if not self.name:
|
| 162 |
+
raise ValueError("name is required for model requests")
|
| 163 |
+
|
| 164 |
+
if self.type == "blog":
|
| 165 |
+
self.title = (self.title or "").strip()
|
| 166 |
+
self.link = (self.link or "").strip()
|
| 167 |
+
self.date = (self.date or "").strip()
|
| 168 |
+
if not self.title:
|
| 169 |
+
raise ValueError("title is required for blog requests")
|
| 170 |
+
if not self.link:
|
| 171 |
+
raise ValueError("link is required for blog requests")
|
| 172 |
+
if not DATE_RE.match(self.date):
|
| 173 |
+
raise ValueError("date must be in YYYY-MM-DD format")
|
| 174 |
+
|
| 175 |
+
return self
|
| 176 |
+
|
| 177 |
# ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 178 |
|
| 179 |
+
def _slugify(text: str) -> str:
|
| 180 |
+
text = re.sub(r"[^a-zA-Z0-9]+", "-", text.strip()).strip("-").lower()
|
| 181 |
+
return text or "entry"
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def _pr_fields(item: "FeedbackItem") -> dict:
|
| 185 |
+
"""Derive the fields a submitter shouldn't have to type by hand."""
|
| 186 |
+
if item.type == "organization":
|
| 187 |
+
return {
|
| 188 |
+
"title": item.name,
|
| 189 |
+
"entry_id": item.org_id,
|
| 190 |
+
"name": item.name,
|
| 191 |
+
"org_id": item.org_id,
|
| 192 |
+
"link": f"https://huggingface.co/{item.org_id}",
|
| 193 |
+
"tags": item.tags,
|
| 194 |
+
}
|
| 195 |
+
if item.type in {"model", "dataset"}:
|
| 196 |
+
org_id = item.slug.split("/", 1)[0]
|
| 197 |
+
fields = {
|
| 198 |
+
"title": f"{item.name} β {item.slug}" if item.name else item.slug,
|
| 199 |
+
"entry_id": _slugify(item.slug),
|
| 200 |
+
"slug": item.slug,
|
| 201 |
+
"org_id": org_id,
|
| 202 |
+
"entry_type": item.entry_type,
|
| 203 |
+
"tags": item.tags,
|
| 204 |
+
}
|
| 205 |
+
if item.name:
|
| 206 |
+
fields["name"] = item.name
|
| 207 |
+
return fields
|
| 208 |
+
if item.type == "blog":
|
| 209 |
+
match = re.match(r"^https?://huggingface\.co/blog/(.+?)/?$", item.link)
|
| 210 |
+
blog_slug = match.group(1) if match else None
|
| 211 |
+
return {
|
| 212 |
+
"title": item.title,
|
| 213 |
+
"entry_id": _slugify(blog_slug or item.title),
|
| 214 |
+
"slug": blog_slug,
|
| 215 |
+
"link": item.link,
|
| 216 |
+
"date": item.date,
|
| 217 |
+
"tags": item.tags,
|
| 218 |
+
}
|
| 219 |
+
return {}
|
| 220 |
+
|
| 221 |
def load_existing(filename: str) -> list[dict]:
|
| 222 |
"""Download the given jsonl file from the dataset, return as a list."""
|
| 223 |
try:
|
|
|
|
| 270 |
if item.type == "collaboration":
|
| 271 |
entry["email"] = item.email
|
| 272 |
entry["institution"] = item.institution
|
| 273 |
+
entry.update(_pr_fields(item))
|
| 274 |
|
| 275 |
target_file = REQUESTS_FILE if item.type in REQUEST_TYPES else FEEDBACK_FILE
|
| 276 |
|