Spaces:
Running
feat(categories): switch to single-label classification
Browse filesEach app now gets EXACTLY ONE category - the dominant slug that
best captures its primary identity - instead of up to 3.
Rationale
Multi-label was producing the same app in multiple mobile-shell
swipers (a music+dance+games app would surface in three category
rows), which made the browse surface feel noisy and duplicated.
Single-label gives every app exactly one home, which is what the
mobile category chips and per-category swipers need.
Changes
- MAX_CATEGORIES_PER_APP: 3 -> 1.
- Prompt OUTPUT FORMAT: "Pick EXACTLY ONE slug" instead of "1 to 3".
- STEP 2 of the DECISION ALGORITHM: explicit tie-break guidance
(which slug a user would name FIRST in one word), with concrete
examples (music+dance, storytelling+kids, vision+games).
- Few-shot: Music Quiz example moved from ["music","games","dance"]
to ["music"] to model the tie-break.
- TAXONOMY_VERSION: 2 -> 3 (forces a reclassify on the next boot).
Side effects (positive)
Single-label naturally resolves the two pitfalls that v2 still had:
- `surf-report`: kids hallucination disappears (only voice remains).
- `marionette-js`: dev-tools-combined-with-dance disappears (only
dance remains, since dev-tools is now the SOLE category when used).
Output shape stays `categories: string[]` (always 0 or 1 entry) for
forward compatibility - reverting to multi-label later would be a
prompt change only, no API break.
Co-authored-by: Cursor <cursoragent@cursor.com>
- server/categories.js +4 -1
- server/categorize.js +27 -11
|
@@ -33,8 +33,11 @@
|
|
| 33 |
* - v1: initial 8-slug taxonomy.
|
| 34 |
* - v2: added `games`, tightened `kids` + `dev-tools` descriptions,
|
| 35 |
* switched the prompt to a DECISION ALGORITHM with few-shot.
|
|
|
|
|
|
|
|
|
|
| 36 |
*/
|
| 37 |
-
export const TAXONOMY_VERSION =
|
| 38 |
|
| 39 |
/**
|
| 40 |
* Canonical category list. Keep slugs short, kebab-case, and
|
|
|
|
| 33 |
* - v1: initial 8-slug taxonomy.
|
| 34 |
* - v2: added `games`, tightened `kids` + `dev-tools` descriptions,
|
| 35 |
* switched the prompt to a DECISION ALGORITHM with few-shot.
|
| 36 |
+
* - v3: switched from multi-label (up to 3 slugs) to single-label
|
| 37 |
+
* (exactly 1 slug). Each app surfaces in exactly one category
|
| 38 |
+
* section on the mobile shell - no duplicates across swipers.
|
| 39 |
*/
|
| 40 |
+
export const TAXONOMY_VERSION = 3;
|
| 41 |
|
| 42 |
/**
|
| 43 |
* Canonical category list. Keep slugs short, kebab-case, and
|
|
@@ -39,7 +39,13 @@ const DEFAULT_MODEL = 'meta-llama/Llama-3.1-8B-Instruct';
|
|
| 39 |
|
| 40 |
// README budget
|
| 41 |
const README_MAX_CHARS = 3000;
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
// LLM call budget
|
| 45 |
const LLM_TIMEOUT_MS = 30_000;
|
|
@@ -175,8 +181,9 @@ const FEW_SHOT_EXAMPLES = [
|
|
| 175 |
[
|
| 176 |
'Music Quiz',
|
| 177 |
'Play a blind test music game with a dancing Reachy.',
|
| 178 |
-
['music'
|
| 179 |
-
'(
|
|
|
|
| 180 |
],
|
| 181 |
[
|
| 182 |
'Mime Bot',
|
|
@@ -217,9 +224,11 @@ function buildMessages({ name, description, readme }) {
|
|
| 217 |
const system = `You classify a Reachy Mini robot app into a CLOSED list of categories.
|
| 218 |
|
| 219 |
OUTPUT FORMAT
|
| 220 |
-
Return ONLY a single JSON object: {"categories": ["
|
| 221 |
-
Pick
|
| 222 |
-
|
|
|
|
|
|
|
| 223 |
|
| 224 |
DECISION ALGORITHM (apply in order)
|
| 225 |
|
|
@@ -231,13 +240,20 @@ raw remote-control interface, dev-only test space.
|
|
| 231 |
Examples that DO NOT pass the veto (they are user-facing apps):
|
| 232 |
TTS players, voice chat, music apps, storytelling, companions -
|
| 233 |
even when the README is dev-heavy.
|
| 234 |
-
- YES -> return {"categories": ["dev-tools"]} and STOP.
|
| 235 |
- NO -> continue to STEP 2.
|
| 236 |
|
| 237 |
-
STEP 2 - Pick
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
If the README is empty or very sparse, USE THE NAME AND DESCRIPTION
|
| 242 |
as the primary signal - do not bail to an empty list just because the
|
| 243 |
README is thin.
|
|
|
|
| 39 |
|
| 40 |
// README budget
|
| 41 |
const README_MAX_CHARS = 3000;
|
| 42 |
+
|
| 43 |
+
// Single-label classification: each app gets EXACTLY ONE slug -
|
| 44 |
+
// the dominant one. The shape stays `string[]` for forward
|
| 45 |
+
// compatibility (if we ever revert to multi-label, no API break),
|
| 46 |
+
// but the array always contains 0 or 1 entry. Mobile chips and
|
| 47 |
+
// "swipers per category" thus surface each app once and only once.
|
| 48 |
+
const MAX_CATEGORIES_PER_APP = 1;
|
| 49 |
|
| 50 |
// LLM call budget
|
| 51 |
const LLM_TIMEOUT_MS = 30_000;
|
|
|
|
| 181 |
[
|
| 182 |
'Music Quiz',
|
| 183 |
'Play a blind test music game with a dancing Reachy.',
|
| 184 |
+
['music'],
|
| 185 |
+
'(single dominant slug - music wins over games/dance because ' +
|
| 186 |
+
"the app's primary identity is a music blind-test)",
|
| 187 |
],
|
| 188 |
[
|
| 189 |
'Mime Bot',
|
|
|
|
| 224 |
const system = `You classify a Reachy Mini robot app into a CLOSED list of categories.
|
| 225 |
|
| 226 |
OUTPUT FORMAT
|
| 227 |
+
Return ONLY a single JSON object: {"categories": ["slug"]}.
|
| 228 |
+
Pick EXACTLY ONE slug - the single dominant category that best
|
| 229 |
+
captures the app's primary identity. Use the EXACT slug. The list
|
| 230 |
+
always contains 0 or 1 entry.
|
| 231 |
+
No prose, no code fences, no commentary outside the JSON.
|
| 232 |
|
| 233 |
DECISION ALGORITHM (apply in order)
|
| 234 |
|
|
|
|
| 240 |
Examples that DO NOT pass the veto (they are user-facing apps):
|
| 241 |
TTS players, voice chat, music apps, storytelling, companions -
|
| 242 |
even when the README is dev-heavy.
|
| 243 |
+
- YES -> return {"categories": ["dev-tools"]} and STOP.
|
| 244 |
- NO -> continue to STEP 2.
|
| 245 |
|
| 246 |
+
STEP 2 - Pick the SINGLE most dominant user-facing slug from the list
|
| 247 |
+
below. Choose the slug that captures the app's primary identity, not
|
| 248 |
+
every aspect it touches. When two slugs feel equally fitting, pick the
|
| 249 |
+
one that a user would name FIRST when describing the app in one word.
|
| 250 |
+
Examples of tie-breaks:
|
| 251 |
+
- music + dance app -> prefer \`music\` if the user picks the song,
|
| 252 |
+
\`dance\` if the choreography is the point.
|
| 253 |
+
- storytelling + kids app -> prefer \`kids\` if it explicitly targets
|
| 254 |
+
children, \`storytelling\` otherwise.
|
| 255 |
+
- vision + games app -> prefer \`games\` if there is a play loop,
|
| 256 |
+
\`vision\` if it is mostly a perception demo.
|
| 257 |
If the README is empty or very sparse, USE THE NAME AND DESCRIPTION
|
| 258 |
as the primary signal - do not bail to an empty list just because the
|
| 259 |
README is thin.
|