Spaces:
Paused
Paused
| # SPEC.md β "First Contact" (working title) | |
| A small-model game for the Hugging Face **Build Small** hackathon (Adventure track). | |
| You teach an alien that knows *words* but has never experienced human life. It | |
| acts in a tiny sandbox world. Over a session it accumulates *concepts* and begins | |
| to generalize them to new situations. The "it finally understood me" moment is the | |
| payoff and the shareable artifact. | |
| This document is the contract. Implement against it exactly. Where it says MUST, | |
| it is load-bearing for either correctness or the hackathon constraints. Where it | |
| says SHOULD, use judgement. | |
| --- | |
| ## 0. Non-negotiable platform constraints (READ FIRST) | |
| These come from the hackathon rules and the current ZeroGPU docs. Violating any of | |
| them either breaks the deploy or disqualifies the entry. | |
| - **The submission IS a Hugging Face Space under the hackathon org.** Modal (below) | |
| is a *development/serving convenience only*, never the deliverable. | |
| - **ZeroGPU requires the Gradio SDK.** Not Streamlit, not Docker. The app MUST be a | |
| Gradio app from the first commit. Do not build a Flask/FastAPI prototype to port | |
| later. | |
| - **Gradio 4+** and **PyTorch β₯ 2.8.0** are required by ZeroGPU. Pin accordingly. | |
| - **`torch.compile` is NOT supported** on ZeroGPU. Do not use it. (Ahead-of-time | |
| compilation exists but is out of scope for this build.) | |
| - **Model MUST be loaded onto `'cuda'` at module level**, NOT lazily inside the | |
| GPU function. CUDA transfers are optimized for startup placement. A PyTorch CUDA | |
| emulation mode makes module-level `.to('cuda')` work outside the GPU function. | |
| - **The GPU-bound function MUST be decorated with `@spaces.GPU`.** Default GPU | |
| runtime is **60 seconds**; if a call can exceed that, pass `duration=` (e.g. | |
| `@spaces.GPU(duration=120)`). Shorter declared durations get better queue | |
| priority β keep it tight. | |
| - **Model MUST be β€ 32B parameters** (hackathon rule). | |
| - **All per-user game state MUST live in `gr.State`**, never module globals. | |
| Module globals are shared across all concurrent visitors β the moment the demo | |
| link is posted, global state means every player shares one alien. This is the | |
| single most common Gradio-on-Spaces bug; do not fall into it. | |
| - **No secrets in code.** HF token via Space secrets / env var. No API keys | |
| anywhere in the repo. | |
| - **No `localStorage` / browser storage.** State is server-side in `gr.State`. | |
| ZeroGPU quota for an org member is **40 min/day of GPU time**. This is plenty for | |
| dev and demo *if* we don't waste it β see Β§6 (the model call MUST be swappable so | |
| that ledger/world logic can be developed against a stub with zero GPU spend). | |
| --- | |
| ## 1. The core architecture in one paragraph | |
| The model never learns in the weights sense. The alien's growing understanding | |
| lives in a **concept ledger** maintained in plain Python and injected into the | |
| prompt every turn. The model is a **stateless function**: given (the alien's | |
| current ledger + the world state + the player's utterance), it returns (an action | |
| in the sandbox + an in-character reply + structured notes about what it did and | |
| didn't understand). The host code applies the action deterministically, checks the | |
| win condition mechanically, and decides whether a new concept was taught. That loop | |
| β not the model β is the game. | |
| ``` | |
| ββββββββββββββββββββββββββββββββββββββββββββ | |
| β gr.State (per session) β | |
| β ledger: list[Concept] β | |
| β world: WorldState β | |
| β challenge: Challenge β | |
| β turn: int β | |
| ββββββββββββββββββββββββββββββββββββββββββββ | |
| β β² | |
| player utterance β β updated state rendered to UI | |
| β βΌ β | |
| βΌ ββββββββββββββββββββββββ | |
| build_prompt(...) βββΆβ model_call(prompt) β β SWAPPABLE (stub | local | modal) | |
| β² β @spaces.GPU β | |
| β ββββββββββββββββββββββββ | |
| β β strict JSON | |
| ledger rendered as βΌ | |
| "things you now parse + validate (retry once, then safe no-op) | |
| understand" β | |
| βΌ | |
| apply_action(world, action) ββ deterministic | |
| β | |
| βΌ | |
| check_win(world, challenge) ββ mechanical, no model | |
| β | |
| βΌ | |
| maybe_learn(ledger, candidate_concept, player_confirm) | |
| ``` | |
| --- | |
| ## 2. Data model | |
| Implement as dataclasses (or pydantic if you prefer validation; dataclasses are | |
| fine). Everything is JSON-serializable so it can live in `gr.State`. | |
| ### 2.1 Concept (an entry in the ledger) | |
| ```python | |
| @dataclass | |
| class Concept: | |
| id: str # stable slug, e.g. "hidden_info" | |
| label: str # short human label, e.g. "hiding information" | |
| player_phrase: str # how the player expressed it when teaching | |
| understanding: str # the alien's internal gloss, 1 sentence, in alien framing | |
| taught_on_turn: int | |
| times_applied: int = 0 | |
| ``` | |
| The ledger starts with ONLY raw physical primitives the alien already has. These | |
| are NOT social/temporal/abstract. Seed exactly these (tune wording, keep the set | |
| small): | |
| - `object` β "a thing that exists in a place" | |
| - `move` β "to change where I am" | |
| - `give_take` β "a thing can pass from one holder to another" | |
| - `more_less` β "quantities can differ" | |
| - `point` β "I can direct attention to a thing" | |
| Everything interesting (`hidden_info`, `secret`, `gift`, `surprise`, `trade`, | |
| `promise`, `lie`, ...) MUST be acquired in-session by teaching. Do NOT seed them. | |
| ### 2.2 WorldState (the deterministic sandbox) | |
| Keep it tiny. The world exists so that **success is checkable without the model | |
| judging semantics.** A reasonable starting world: | |
| ```python | |
| @dataclass | |
| class Obj: | |
| id: str | |
| name: str # "blue stone", "red stone", "basket" | |
| location: str # an Agent id, a Container id, or "ground" | |
| hidden: bool = False # concealed from other agents? | |
| @dataclass | |
| class Agent: | |
| id: str # "alien", "other" (the alien, and a second NPC) | |
| name: str | |
| holding: list[str] # obj ids | |
| @dataclass | |
| class WorldState: | |
| objects: dict[str, Obj] | |
| agents: dict[str, Agent] | |
| containers: list[str] # e.g. ["basket"] β things that can conceal | |
| log: list[str] # human-readable record of actions taken | |
| ``` | |
| ### 2.3 Action (what the model is allowed to do to the world) | |
| The action space MUST be small, closed, and enumerable. The model picks ONE per | |
| turn. This is what makes the system robust β the model can say anything in its | |
| *reply*, but it can only *do* things from this list, so application is | |
| deterministic and unsurprising. | |
| Verbs (starting set β expand only if a challenge needs it): | |
| | verb | args | effect | | |
| |-------------|-------------------------------|-----------------------------------------------------| | |
| | `move_to` | `target: location` | alien moves | | |
| | `pick_up` | `obj_id` | alien adds obj to `holding`, obj.location = "alien" | | |
| | `put_in` | `obj_id, container_id` | obj.location = container; if container conceals, obj.hidden = True | | |
| | `give` | `obj_id, agent_id` | transfer obj to another agent's holding | | |
| | `point_at` | `obj_id | agent_id` | no world change; signals attention | | |
| | `wait` | β | no-op (the confused/contemplative fallback) | | |
| ```python | |
| @dataclass | |
| class Action: | |
| verb: str # one of the above | |
| args: dict # validated against the verb's signature | |
| ``` | |
| ### 2.4 Challenge (the current goal, with a MECHANICAL win condition) | |
| ```python | |
| @dataclass | |
| class Challenge: | |
| id: str | |
| title: str # "Teach the alien to hide the stone" | |
| setup_blurb: str # shown to the player | |
| teaches: str | None # concept id this challenge is designed to introduce (or None if it tests generalization) | |
| win_predicate: Callable[[WorldState], bool] # checked after each action | |
| # generalization challenges set `teaches=None` and rely on a previously | |
| # learned concept being applied to a NEW situation. | |
| ``` | |
| **Win is ALWAYS a predicate over WorldState, never a semantic judgement by the | |
| model.** Example: "hide the blue stone from `other`" β | |
| `lambda w: w.objects["blue_stone"].hidden and w.objects["blue_stone"].location == "basket"`. | |
| --- | |
| ## 3. The model contract (strict JSON) | |
| The model MUST return ONLY a JSON object, no prose around it, matching this schema: | |
| ```json | |
| { | |
| "action": { "verb": "put_in", "args": { "obj_id": "blue_stone", "container_id": "basket" } }, | |
| "utterance": "I place the blue-thing inside the holder. The other cannot see it now?", | |
| "gap": "I do not understand why you want the other to not-see", | |
| "candidate_concept": { | |
| "id": "hidden_info", | |
| "label": "hiding information", | |
| "understanding": "one mind can hold a thing another mind does not have, on purpose" | |
| } | |
| } | |
| ``` | |
| Field rules: | |
| - `action` β REQUIRED. `verb` MUST be in the allowed set; `args` MUST match the | |
| verb. If the model emits an unknown verb or bad args β treated as a parse | |
| failure (see Β§4). | |
| - `utterance` β REQUIRED. The alien's in-character reply. This is where the voice | |
| and the comedy live. | |
| - `gap` β nullable. The alien naming what it could NOT do/understand. Drives both | |
| the humour and the player's sense of what to teach next. `null` when the alien | |
| understood fully. | |
| - `candidate_concept` β nullable. The alien proposing "I think you just taught me | |
| a new primitive." When present and coherent, it becomes a ledger entry (gated β | |
| see Β§5). `null` on most turns. | |
| **Getting a small model to emit clean JSON reliably is the #1 engineering risk.** | |
| Mitigations, in order of preference, implement at least the first two: | |
| 1. Use the model's **chat template** and a system prompt that ends with the exact | |
| JSON schema and the instruction to output nothing else. | |
| 2. **Constrained / grammar-guided decoding** if the serving stack supports it | |
| (e.g. `outlines`, `lmformatenforcer`, or transformers' JSON mode). This nearly | |
| eliminates parse failures and is worth the setup. | |
| 3. As a floor: a tolerant parser that extracts the first balanced `{...}` block | |
| from the output before `json.loads`. | |
| --- | |
| ## 4. Robustness (the JSON parse-fail path) | |
| The model WILL occasionally produce malformed output. The system MUST degrade | |
| gracefully, never crash, never leak a stack trace to the player. | |
| ``` | |
| call model | |
| ββ parse JSON | |
| ββ success + valid action β proceed | |
| ββ failure (bad JSON | unknown verb | bad args) | |
| ββ re-prompt ONCE, appending: "Your previous reply was not valid. | |
| Error: <msg>. Respond again, JSON only, matching the schema." | |
| ββ success β proceed | |
| ββ failure again β SAFE FALLBACK: | |
| action = {"verb": "wait"} | |
| utterance = "<the alien looks at you, not understanding>" | |
| gap = "I could not grasp that" | |
| candidate_concept = null | |
| ``` | |
| The safe fallback is in-character (the alien being confused is *consistent with the | |
| fiction*), which is why this game tolerates model failure better than most. | |
| Budget real time for this path. It is not optional polish; it is what keeps the | |
| live demo from dying on stage. | |
| --- | |
| ## 5. The learning loop (gating ledger additions) | |
| Do NOT silently append every `candidate_concept` β that pollutes the ledger and | |
| removes the player's sense of agency. Gate it: | |
| - A `candidate_concept` is added to the ledger only when **the player confirms** | |
| (a lightweight "Yes, it learned that" affordance in the UI) OR when it is clearly | |
| coherent and non-duplicative (configurable; start with explicit player confirm so | |
| the "it learned!" beat is deliberate and screenshot-worthy). | |
| - On addition: assign `taught_on_turn = current turn`, set `times_applied = 0`. | |
| - When a subsequent turn's chosen action depends on an existing concept (heuristic: | |
| the prompt-builder injected it and the model's `gap` is null on a situation that | |
| would previously have produced a gap), increment `times_applied`. This is what | |
| powers the "constellation grows / concept lights up when reused" UI moment. | |
| **The generalization beat** (the emotional core): a Challenge with `teaches=None` | |
| presents a NEW situation that a *previously taught* concept should cover. Success | |
| is the alien spontaneously applying e.g. `hidden_info` to understand "secret" or | |
| "surprise" without being re-taught. Author at least two of these (see Β§8). | |
| --- | |
| ## 6. The swappable model interface (PROTECT YOUR GPU QUOTA) | |
| The model call MUST sit behind a single interface with at least three | |
| implementations. This is both good design and the thing that lets the entire | |
| ledger/world/challenge logic be built and tested with **zero GPU spend**. | |
| ```python | |
| class Brain(Protocol): | |
| def respond(self, prompt: str) -> str: # returns raw model text | |
| ... | |
| # 1) StubBrain β deterministic, no GPU. Returns canned valid-JSON responses keyed | |
| # to test scenarios. Develop the ENTIRE loop against this first. | |
| # 2) LocalBrain β transformers model on 'cuda', loaded at module level, called | |
| # inside @spaces.GPU. The real ZeroGPU path. | |
| # 3) ModalBrain β calls a Modal endpoint (see Β§7). Optional; for when ZeroGPU | |
| # queues are bad at peak, or for the fine-tune. NOT the submission path. | |
| ``` | |
| Select implementation via env var (`BRAIN=stub|local|modal`), default `stub` | |
| locally and `local` on the Space. Day-one development happens almost entirely on | |
| `StubBrain`. | |
| `@spaces.GPU` wraps only the `LocalBrain.respond` call. State mutation | |
| (ledger/world/win-check) happens OUTSIDE the decorated function so it never holds | |
| the GPU. | |
| --- | |
| ## 7. Modal (optional, dev/serving only β NEVER the submission) | |
| Reference: `github.com/modal-labs/modal-examples`, folder `06_gpu_and_ml` | |
| (LLM fine-tuning + serving), `04_secrets` (HF token pattern). The repo's examples | |
| are tested on Python 3.11; match that for the Modal side to avoid surprises. | |
| Two legitimate uses, both behind `ModalBrain`: | |
| 1. **Serving the model** as an HTTP endpoint when ZeroGPU queues are slow during | |
| peak hours. The Space's `ModalBrain` calls it. Keep the Space the deliverable. | |
| 2. **The optional LoRA fine-tune** (badge): use the $250 Modal credits to train a | |
| small LoRA that fixes JSON-formatting reliability and locks the alien voice. Do | |
| this ONLY if the prompt-only `LocalBrain` is genuinely struggling on those two | |
| axes β do not manufacture the need just because credits exist. If you do it, the | |
| resulting adapter loads in `LocalBrain` for the real submission. | |
| Modal MUST NOT be required for the Space to run. If `BRAIN != modal`, no Modal | |
| dependency should be imported. | |
| --- | |
| ## 8. Content: the challenge arc | |
| Authoring is where delight is won, not engineering. Keep the arc SHORT β | |
| **5β6 challenges**, not 15. Scope creep on content eats the UI/submission time. | |
| Suggested arc (the spine β refine the wording later): | |
| 1. **Warm-up (teaches `object`/`move` are enough):** "Put the red stone in the | |
| basket." Pure mechanical, teaches the player the interaction model and shows the | |
| alien being literal-competent. No new concept. | |
| 2. **First real teach (`hidden_info`):** "Hide the blue stone from the other one." | |
| The alien has `put_in` but no concept of *concealment-as-information-state*. The | |
| player must teach "you can keep a thing so another mind does not have it." | |
| 3. **Build on it (`gift`/`give_take`+intent):** "Give the other a present." The | |
| alien has `give` mechanically but no concept of *gift* (transfer + positive | |
| intent). Teach it. | |
| 4. **GENERALIZATION (teaches=None, relies on `hidden_info`):** "Make a surprise for | |
| the other." Success = the alien combines hiding (concealment) + gift (giving) | |
| *without being re-taught either* β it generalizes `hidden_info` to "surprise = | |
| gift they don't know about yet." THIS is the headline moment. | |
| 5. **Optional stretch (`trade`/`promise`):** something that requires composing two | |
| learned concepts. Author only if time allows. | |
| Each challenge: `setup_blurb` for the player, a `win_predicate` over WorldState, | |
| and (for teach challenges) the target concept id. | |
| --- | |
| ## 9. UI (Gradio, custom-styled β the "Off-Brand" badge) | |
| Build with `gr.Blocks` + custom CSS. The aesthetic SHOULD be committed and | |
| distinctive (this is a judged "would you show a friend" entry, and there's a badge | |
| for not looking like default Gradio). Apply these principles: | |
| - **Commit to one strong aesthetic direction.** Something that fits "first contact | |
| with a strange mind" β e.g. a quiet, alien, almost-archival feel; or a warm | |
| field-notes/xenolinguist's-journal feel. Pick one and execute it precisely. Avoid | |
| default Gradio greys and avoid generic purple-gradient-on-white "AI" look. | |
| - **Distinctive type.** A characterful display font for the alien's voice paired | |
| with a clean readable body font. Not Inter/Roboto/Arial defaults. | |
| - **The three panels:** | |
| 1. **The world** β a simple visual (even CSS/SVG boxes is fine) showing | |
| objects/agents/containers and their state. The `hidden` flag must be *visibly* | |
| represented (e.g. an object inside the basket shown as concealed). | |
| 2. **The conversation** β player utterances + the alien's replies. The alien's | |
| `gap` SHOULD be shown distinctly (a muted "β¦did not understandβ¦" line) because | |
| it's both funny and instructive. | |
| 3. **The ledger** β the learned concepts, rendered as a *growing* set (a | |
| constellation, a glossary, a stack of field-notes). When a concept is newly | |
| learned or re-applied, it SHOULD visibly light up / animate once. This is the | |
| screenshot. | |
| - **One well-orchestrated reveal beats scattered micro-animations.** Put the polish | |
| budget on the "concept learned" / "generalization succeeded" moment. | |
| - **The success state** (esp. the generalization challenge) MUST be unmistakable and | |
| shareable β a clear "it understood you" beat the player will want to screenshot | |
| for the required social post. | |
| Keep backgrounds atmospheric, not flat. No browser-storage. Everything reactive via | |
| `gr.State` updates. | |
| --- | |
| ## 10. Repo layout | |
| ``` | |
| . | |
| βββ app.py # Gradio Blocks UI + turn loop wiring. Space entrypoint. | |
| βββ README.md # Space card (YAML header: sdk: gradio, sdk_version, hardware) + how-to | |
| βββ requirements.txt # pinned; see Β§0 for version floors | |
| βββ game/ | |
| β βββ __init__.py | |
| β βββ models.py # dataclasses: Concept, Obj, Agent, WorldState, Action, Challenge | |
| β βββ ledger.py # seed primitives, add/gate, times_applied tracking | |
| β βββ world.py # apply_action (deterministic), initial world factory | |
| β βββ challenges.py # the 5β6 challenges + win predicates (Β§8) | |
| β βββ prompt.py # build_prompt(ledger, world, challenge, utterance) -> str | |
| β βββ parsing.py # tolerant JSON extract + validate + the Β§4 retry/fallback | |
| β βββ brain.py # Brain protocol + StubBrain | LocalBrain | ModalBrain (Β§6) | |
| βββ tests/ | |
| β βββ test_world.py # apply_action correctness, win predicates | |
| β βββ test_parsing.py # malformed-output handling, fallback path | |
| β βββ test_loop_stub.py # full turn loop against StubBrain, zero GPU | |
| βββ modal/ # OPTIONAL, only if Β§7 is used | |
| βββ serve.py # Modal serving endpoint | |
| βββ finetune_lora.py # optional LoRA training job | |
| ``` | |
| --- | |
| ## 11. Build order (maps to the two-weekend window) | |
| This is spine-first on purpose. The classic hackathon death is polishing | |
| disconnected pieces that never form a loop. | |
| **Weekend 1 β make the loop real** | |
| - **Day 1 (Fri eve / Sat):** | |
| - `game/models.py`, `game/world.py` (apply_action + initial world), `game/ledger.py` | |
| (seed primitives). | |
| - `game/parsing.py` with the full Β§4 fallback. | |
| - `StubBrain` returning canned valid JSON for challenge #1 and #2. | |
| - Minimal `app.py`: one hardcoded challenge, the turn loop wired end-to-end | |
| against `StubBrain`, world + conversation rendering. Ugly is fine. | |
| - `tests/test_world.py`, `tests/test_loop_stub.py` green. **All on zero GPU.** | |
| - Stand up the empty Space early and push, to shake out the deploy/secrets/env | |
| before there's anything to lose. | |
| - **Day 2 (Sun):** | |
| - `LocalBrain` on the Space: model to `'cuda'` at module level, `respond` inside | |
| `@spaces.GPU(duration=...)`. Pick the model by testing 3β4 β€32B instruct models | |
| on the Β§3 prompt for **JSON-formatting reliability first**, capability second. | |
| - Ledger gating + `candidate_concept` flow + the `times_applied` increment. | |
| - Challenge #4 (the generalization beat) authored and working against the real | |
| model. **If the generalization doesn't feel magical here, the concept design | |
| needs adjusting and you still have a week to pivot.** | |
| **Midweek (evenings) β harden + de-risk** | |
| - Tighten the system prompt so the alien voice stays consistently *alien* and | |
| doesn't drift to helpful-assistant. Add constrained decoding (Β§3.2) if parse | |
| failures are common. | |
| - This is the fine-tune decision point (Β§7): only if prompt-only is fighting you on | |
| JSON or voice. | |
| - Catch the hackathon AMA if it lands midweek; ask specifically about ZeroGPU quota | |
| behaviour and JSON-mode tricks. | |
| **Weekend 2 β content, UI, ship** | |
| - **Day 1 (Sat):** finish the challenge arc (Β§8), then the custom-styled UI pass | |
| (Β§9) with the polish budget concentrated on the "concept learned" / | |
| "generalization succeeded" reveal. | |
| - **Day 2 (Sun):** the submission is itself graded β Space under the org + demo | |
| video + social post. Budget the back half of Sunday for it. Script the demo | |
| around the single best generalization moment. Submit with margin, not at the | |
| deadline. | |
| --- | |
| ## 12. Definition of done | |
| - Runs as a Gradio app on a ZeroGPU Space under the hackathon org, model β€32B, | |
| loaded at module level, inference in `@spaces.GPU`. | |
| - A fresh visitor gets their own session state (verified: two browsers don't share | |
| an alien). | |
| - The full arc is playable: literal warm-up β first concept taught β at least one | |
| generalization beat where a learned concept transfers to a new situation | |
| unprompted. | |
| - Malformed model output never crashes the app; the alien "looks confused" instead. | |
| - The ledger visibly grows and a concept lights up on learn/re-use. | |
| - Demo video + social post produced, both centred on the generalization moment. | |
| - (Optional badges as time allows: LoRA fine-tune, custom UI β UI is in scope | |
| regardless.) | |
| --- | |
| ## 13. Things NOT to do | |
| - Do NOT let the model judge whether communication "succeeded" β success is always | |
| a WorldState predicate. | |
| - Do NOT seed social/abstract concepts in the ledger β they must be taught. | |
| - Do NOT use module globals for game state. | |
| - Do NOT lazy-load the model inside `@spaces.GPU`. | |
| - Do NOT use `torch.compile` (unsupported on ZeroGPU). | |
| - Do NOT make Modal a hard dependency of the Space. | |
| - Do NOT burn ZeroGPU quota developing the loop β that's what `StubBrain` is for. | |
| - Do NOT expand the challenge arc past ~6 β content scope creep kills the UI/submission time. | |