EmmaScharfmann HF Staff Claude Sonnet 5 commited on
Commit
02a536f
Β·
1 Parent(s): 13525d6

Accept structured fields so reviewers can approve with one click

Browse files

Submissions 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>

Files changed (1) hide show
  1. app.py +117 -0
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