Spaces:
Running on Zero
Running on Zero
| """Shared taxonomy and prompt format for quest classification. | |
| The dashboard refresh asks MiniCPM5-1B to classify each hackathon project against | |
| the Build Small Hackathon judging dimensions. Beyond the six merit-badge side | |
| quests the advisor already tracks, the contest also runs two main tracks and a set | |
| of sponsor / special awards that are equally detectable from a project's README and | |
| app file (which model it loads, whether it runs on Modal, whether it is agentic). | |
| This module is the single source of truth for that label space and for the strict | |
| two-segment prompt, so the LoRA training data and the live analyzer stay aligned. | |
| Output schema (one JSON object, nothing else): | |
| {"matches": [{"quest": str, "confidence": 0.0-1.0, "evidence": str, | |
| "source": "readme" | "app_file"}]} | |
| """ | |
| from __future__ import annotations | |
| from collections.abc import Mapping, Sequence | |
| import json | |
| import re | |
| from typing import Any | |
| SOURCE_README = "readme" | |
| SOURCE_APP_FILE = "app_file" | |
| QUEST_SOURCES = (SOURCE_README, SOURCE_APP_FILE) | |
| # Canonical system prompt shared by the SFT dataset and the live analyzer so the | |
| # model is trained and served under the exact same instruction. | |
| QUEST_SYSTEM_PROMPT = ( | |
| "You classify hackathon projects against fixed quest dimensions. " | |
| "Return exactly one strict JSON object and nothing else. " | |
| "The first character must be { and the last character must be }. " | |
| "Each match needs quest, confidence, evidence, and source (readme or app_file). " | |
| "Never emit markdown, prose, a top-level array, extra keys, or an unknown or rephrased quest name." | |
| ) | |
| # README / app-file budgets used when rendering a project into the prompt. Kept | |
| # small enough that prompt + completion fit the LoRA max_seq_length with headroom. | |
| README_PROMPT_CHAR_LIMIT = 1500 | |
| APP_PROMPT_CHAR_LIMIT = 1900 | |
| # Ordered label space. The first six ids match the merit-badge GOALS the advisor | |
| # already uses elsewhere; the rest are the tracks and sponsor / special awards. | |
| QUEST_PROFILES: tuple[dict[str, str], ...] = ( | |
| { | |
| "id": "Off the Grid", | |
| "label": "Local-first", | |
| "description": "Runs the model on-device with no remote inference call: weights load locally and " | |
| "inference happens in-process, not over a hosted API.", | |
| "signals": "AWARD on a local in-process load: from_pretrained / pipeline / llama_cpp / diffusers / " | |
| "vLLM / ONNX, GGUF weights, @spaces.GPU. DISQUALIFY (do NOT award) on ANY remote inference call, even " | |
| "via huggingface_hub: InferenceClient, HF Inference API/Endpoints, gradio_client to a remote Space, " | |
| "replicate/together/openrouter/fal/groq, a *.modal.run or other HTTP inference endpoint, or " | |
| "openai/anthropic/gemini/cohere clients. A remote call disqualifies regardless of which model it names.", | |
| }, | |
| { | |
| "id": "Well-Tuned", | |
| "label": "Fine-tuned", | |
| "description": "Uses or publishes a fine-tuned or LoRA-adapted model rather than only stock checkpoints.", | |
| "signals": "LoRA/PEFT adapter, fine-tuned model repo, training script, words like fine-tune, adapter, SFT, distilled.", | |
| }, | |
| { | |
| "id": "Off-Brand", | |
| "label": "Custom frontend", | |
| "description": "Ships a custom interface beyond default Gradio styling, with a memorable look or voice.", | |
| "signals": "custom CSS/HTML/JS, gr.HTML, gr.Blocks theme/css=, gr.Server, custom components, bespoke theming.", | |
| }, | |
| { | |
| "id": "Llama Champion", | |
| "label": "llama.cpp path", | |
| "description": "Runs a model through the llama.cpp runtime.", | |
| "signals": "llama-cpp-python, from llama_cpp import Llama, GGUF file, llama.cpp, Llama( constructor.", | |
| }, | |
| { | |
| "id": "Sharing is Caring", | |
| "label": "Shareable artifact", | |
| "description": "Produces an output people can save, post, or compare, or publishes an agent trace to the Hub.", | |
| "signals": "download/export button, gr.File/gr.DownloadButton, save PNG/PDF/JSON, push_to_hub of a trace or dataset.", | |
| }, | |
| { | |
| "id": "Field Notes", | |
| "label": "Build notes", | |
| "description": "Documents the build itself with notes, a write-up, or a blog/report link.", | |
| "signals": "README has a substantial build write-up, devlog, lessons learned, or a blog/report/Notion link.", | |
| }, | |
| { | |
| "id": "Backyard AI", | |
| "label": "Real problem for one person", | |
| "description": "Solves a concrete real-world problem for a specific, named person or persona.", | |
| "signals": "README frames a real user and task (caregiving, a relative, a job, a household chore), practical utility.", | |
| }, | |
| { | |
| "id": "Thousand Token Wood", | |
| "label": "Delightful & creative", | |
| "description": "A delightful, playful, or artistic experience that would not exist without AI.", | |
| "signals": "story/game/art/whimsy framing, generative characters or worlds, playful tone, creative novelty.", | |
| }, | |
| { | |
| "id": "OpenBMB", | |
| "label": "OpenBMB model", | |
| "description": "Uses a model published by OpenBMB (the openbmb org), such as the MiniCPM family.", | |
| "signals": "The model id org prefix must be exactly openbmb/ (openbmb/MiniCPM*, OpenCPM). A model from " | |
| "any other org is NOT OpenBMB: openai/gpt-oss, Qwen/..., meta-llama/..., google/..., nvidia/..., " | |
| "microsoft/..., mistralai/... do NOT count just because a model id is present.", | |
| }, | |
| { | |
| "id": "Nemotron", | |
| "label": "NVIDIA Nemotron", | |
| "description": "Uses an NVIDIA Nemotron model (Nemotron LLM, Parakeet, Nemotron-Speech, Canary).", | |
| "signals": "model repo nvidia/...nemotron..., Parakeet, nemotron-speech, Canary ASR.", | |
| }, | |
| { | |
| "id": "Modal", | |
| "label": "Modal-powered", | |
| "description": "Uses Modal for training, inference, or background compute.", | |
| "signals": "import modal, modal.App, @app.function, Modal endpoint/volume, README cites Modal compute.", | |
| }, | |
| { | |
| "id": "Tiny Titan", | |
| "label": "Small model (<=4B)", | |
| "description": "Runs on a genuinely small model of about four billion parameters or fewer.", | |
| "signals": "AWARD when the model name says <=4B: 0.5B/1B/1.5B/2B/3B/4B or tiny/small/nano/mini " | |
| "(Qwen2.5-1.5B, MiniCPM5-1B, gemma-2b). Do NOT award for 7B/8B/12B/13B/20B/27B/35B+ models " | |
| "(e.g. gpt-oss-20b, Qwen2.5-7B); a version number like V-4.6 is not a parameter count.", | |
| }, | |
| { | |
| "id": "Best Agent", | |
| "label": "Agentic", | |
| "description": "An agentic build: tool use, function calling, planning, or an autonomous multi-step loop.", | |
| "signals": "tool/function calling, an agent/planner loop, multiple orchestrated tools, ReAct, multi-step reasoning over tools.", | |
| }, | |
| ) | |
| QUESTS: tuple[str, ...] = tuple(profile["id"] for profile in QUEST_PROFILES) | |
| QUEST_PROFILE_BY_ID: dict[str, dict[str, str]] = {profile["id"]: profile for profile in QUEST_PROFILES} | |
| def _quest_key(raw: Any) -> str: | |
| text = " ".join(str(raw or "").replace("&", " and ").casefold().split()) | |
| return re.sub(r"[^a-z0-9]+", " ", text).strip() | |
| _QUEST_ALIASES: dict[str, str] = {} | |
| for _profile in QUEST_PROFILES: | |
| _QUEST_ALIASES[_quest_key(_profile["id"])] = _profile["id"] | |
| _QUEST_ALIASES[_quest_key(_profile["label"])] = _profile["id"] | |
| _QUEST_ALIASES[_quest_key(f"Best {_profile['id']}")] = _profile["id"] | |
| _QUEST_ALIASES[_quest_key(f"Best {_profile['label']}")] = _profile["id"] | |
| _QUEST_ALIASES[_quest_key(f"Best Use of {_profile['id']}")] = _profile["id"] | |
| _QUEST_ALIASES[_quest_key(f"Best Use of {_profile['label']}")] = _profile["id"] | |
| _QUEST_ALIASES.update( | |
| { | |
| _quest_key("Best MiniCPM Build"): "OpenBMB", | |
| _quest_key("MiniCPM Build"): "OpenBMB", | |
| _quest_key("MiniCPM"): "OpenBMB", | |
| _quest_key("OpenBMB / MiniCPM"): "OpenBMB", | |
| _quest_key("Small model <=4B"): "Tiny Titan", | |
| _quest_key("Small model under 4B"): "Tiny Titan", | |
| _quest_key("Shareable output"): "Sharing is Caring", | |
| _quest_key("Custom UI"): "Off-Brand", | |
| _quest_key("Custom interface"): "Off-Brand", | |
| _quest_key("Local first"): "Off the Grid", | |
| _quest_key("Fine tuned"): "Well-Tuned", | |
| _quest_key("Fine tune"): "Well-Tuned", | |
| } | |
| ) | |
| def quest_profiles() -> list[dict[str, str]]: | |
| return [ | |
| {"id": profile["id"], "label": profile["label"], "description": profile["description"]} | |
| for profile in QUEST_PROFILES | |
| ] | |
| def quest_label(quest: str) -> str: | |
| return QUEST_PROFILE_BY_ID.get(quest, {}).get("label", quest) | |
| def canonical_quest_id(raw_quest: Any) -> str: | |
| quest = " ".join(str(raw_quest or "").split()) | |
| if quest in QUEST_PROFILE_BY_ID: | |
| return quest | |
| alias = _QUEST_ALIASES.get(_quest_key(quest)) | |
| if alias: | |
| return alias | |
| folded = quest.casefold() | |
| for known in QUESTS: | |
| known_folded = known.casefold() | |
| if folded == known_folded: | |
| return known | |
| if folded.startswith(f"{known_folded} (") or folded.startswith(f"{known_folded} - "): | |
| return known | |
| raise ValueError(f"unknown quest: {quest!r}") | |
| def canonical_quest_ids(raw_quest: Any) -> tuple[str, ...]: | |
| quest = " ".join(str(raw_quest or "").split()) | |
| try: | |
| return (canonical_quest_id(quest),) | |
| except ValueError as original_error: | |
| parts = [part.strip() for part in re.split(r"\s*/\s*", quest) if part.strip()] | |
| if len(parts) <= 1: | |
| raise original_error | |
| canonical: list[str] = [] | |
| for part in parts: | |
| try: | |
| quest_id = canonical_quest_id(part) | |
| except ValueError as error: | |
| raise ValueError(f"unknown quest in composite {quest!r}: {part!r}") from error | |
| if quest_id not in canonical: | |
| canonical.append(quest_id) | |
| return tuple(canonical) | |
| def _clip(text: str, limit: int) -> str: | |
| cleaned = (text or "").strip() | |
| if len(cleaned) <= limit: | |
| return cleaned | |
| return cleaned[:limit].rstrip() + " ..." | |
| _IMPORT_RE = re.compile(r"^\s*(?:import\s+\w|from\s+\w[\w.]*\s+import)\b") | |
| _REPO_ID_RE = re.compile(r"\b[\w-]+/[\w.\-]+\b") | |
| def build_readme_segment(readme_body: str) -> str: | |
| return " ".join(str(readme_body or "").split())[: README_PROMPT_CHAR_LIMIT * 2] | |
| def build_app_segment(app_source: str, app_signals: str = "") -> str: | |
| """Compose an app-file view that keeps imports and asset ids inside budget. | |
| Gradio apps front-load the decisive quest signals (which library is imported, | |
| which model repo is loaded) but a deep model id can fall outside a head slice, | |
| so imports are hoisted and any repo-id-looking tokens from the AST signals that | |
| are still missing are appended as a compact ASSETS line. The SFT dataset and the | |
| live analyzer both call this so the model sees the same app view either way. | |
| """ | |
| source = str(app_source or "") | |
| if not source.strip() and not str(app_signals or "").strip(): | |
| return "" | |
| imports = [line.strip() for line in source.splitlines() if _IMPORT_RE.match(line)] | |
| seen: set[str] = set() | |
| ordered_imports = [imp for imp in imports if not (imp in seen or seen.add(imp))][:40] | |
| head_budget = APP_PROMPT_CHAR_LIMIT * 2 | |
| parts: list[str] = [] | |
| if ordered_imports: | |
| parts.append("\n".join(ordered_imports)) | |
| parts.append(source) | |
| composed = "\n\n".join(parts)[:head_budget] | |
| repo_ids = {token for token in _REPO_ID_RE.findall(app_signals or "") if "/" in token} | |
| missing = sorted(rid for rid in repo_ids if rid not in composed) | |
| if missing: | |
| composed = f"{composed}\n\nASSETS: {', '.join(missing[:12])}" | |
| return composed | |
| def render_quest_prompt( | |
| *, | |
| title: str, | |
| sdk: str, | |
| declared_models: Sequence[str], | |
| tags: Sequence[str], | |
| readme_segment: str, | |
| app_file_name: str, | |
| app_file_segment: str, | |
| include_signals: bool = True, | |
| ) -> str: | |
| """Render the canonical two-segment classification prompt. | |
| The same renderer feeds both the SFT dataset and the live analyzer so the model | |
| never sees a different shape at training and inference time. | |
| """ | |
| quest_lines = [f"- {profile['id']}: {profile['description']}" for profile in QUEST_PROFILES] | |
| if include_signals: | |
| quest_lines = [ | |
| f"- {profile['id']}: {profile['description']} Signals: {profile['signals']}" | |
| for profile in QUEST_PROFILES | |
| ] | |
| readme_text = _clip(readme_segment, README_PROMPT_CHAR_LIMIT) or "(no README description provided)" | |
| app_label = app_file_name.strip() or "(unknown)" | |
| app_text = _clip(app_file_segment, APP_PROMPT_CHAR_LIMIT) or "(no app file available)" | |
| metadata = { | |
| "title": (title or "").strip(), | |
| "sdk": (sdk or "").strip(), | |
| "declared_models": [str(model) for model in declared_models or []], | |
| "tags": [str(tag) for tag in tags or []], | |
| } | |
| return "\n".join( | |
| [ | |
| "Classify this hackathon project against the quest dimensions below.", | |
| "Read the two evidence segments (README and APP_FILE) and judge each quest only from them.", | |
| "", | |
| "Quests (copy the id on the left verbatim):", | |
| *quest_lines, | |
| "", | |
| "Rules:", | |
| "- Include a quest only when a segment gives clear, specific evidence.", | |
| "- quest must be one id from the list above, copied exactly. Never invent or rephrase a quest name.", | |
| "- confidence is a number between 0 and 1.", | |
| "- evidence is a 3-to-12 word quote or tight paraphrase taken from the segment you cite.", | |
| '- source is "readme" when the evidence is in the README segment, "app_file" when it is in the APP_FILE segment.', | |
| "- At most one match per quest. Sort matches by confidence, highest first.", | |
| "- If no quest has clear evidence, return an empty matches list.", | |
| '- Output exactly one JSON object: {"matches":[{"quest":"...","confidence":0.0,"evidence":"...","source":"readme"}]}.', | |
| "- No markdown, no code fences, no commentary, no extra keys.", | |
| "", | |
| f"METADATA: {json.dumps(metadata, ensure_ascii=False)}", | |
| "", | |
| "[README]", | |
| readme_text, | |
| "", | |
| f"[APP_FILE] {app_label}", | |
| app_text, | |
| ] | |
| ) | |
| def normalize_match(match: Mapping[str, Any], *, evidence_limit: int = 360) -> dict[str, Any]: | |
| """Validate and canonicalize one match dict. Raises ValueError on schema drift.""" | |
| quest = canonical_quest_id(match.get("quest")) | |
| try: | |
| confidence = float(match.get("confidence")) | |
| except (TypeError, ValueError) as error: | |
| raise ValueError("confidence must be numeric") from error | |
| if not 0.0 < confidence <= 1.0: | |
| raise ValueError("confidence must be greater than 0 and no more than 1") | |
| evidence = " ".join(str(match.get("evidence") or "").split()) | |
| if not evidence: | |
| raise ValueError("evidence must not be empty") | |
| if _looks_like_prompt_taxonomy(evidence): | |
| raise ValueError("evidence must come from README or APP_FILE, not quest instructions") | |
| source = str(match.get("source") or "") | |
| if source not in QUEST_SOURCES: | |
| raise ValueError(f"source must be one of {QUEST_SOURCES}, got {source!r}") | |
| return { | |
| "quest": quest, | |
| "confidence": round(confidence, 3), | |
| "evidence": evidence[:evidence_limit], | |
| "source": source, | |
| } | |
| def _looks_like_prompt_taxonomy(evidence: str) -> bool: | |
| normalized = " ".join(evidence.casefold().split()) | |
| if "signals:" in normalized: | |
| return True | |
| return any( | |
| normalized.startswith(" ".join(profile[field].casefold().split())[:80]) | |
| for profile in QUEST_PROFILES | |
| for field in ("description",) | |
| ) | |