Claude Opus 4.8 (1M context)
Docs: align CONTRACT/STATE/PLAN/README/todo with the story pivot + 27 tones
cc400af |
Download docs/CONTRACT.md from build-small-hackathon/forgotten-lily: direct link, hf CLI and curl.
- Browser
- Download file 6.66 kB
-
https://huggingface.co/spaces/build-small-hackathon/forgotten-lily/resolve/main/docs/CONTRACT.md
- Command line
-
hf download hf://spaces/build-small-hackathon/forgotten-lily/docs/CONTRACT.md
-
curl -L -o CONTRACT.md https://huggingface.co/spaces/build-small-hackathon/forgotten-lily/resolve/main/docs/CONTRACT.md
6.66 kB
| # Inference Contract β Gemma β Frontend | |
| The single source of truth for the data crossing between the game server and the | |
| inference layer. Both `app/mock_inference.py` and the real Modal/Gemma function | |
| MUST honor this exactly. Everything downstream (chips, audio, notes, lexicon) keys | |
| off these shapes. | |
| --- | |
| ## Turn request β server β inference | |
| ```json | |
| { | |
| "session_id": "uuid", | |
| "message": "what do you remember about him?", | |
| "turn": 14, | |
| "act": 2, | |
| "mood": "wistful", | |
| "active_tones": ["self_i", "self_feel", "other_you", "emo_happy", "emo_sad", | |
| "truth_yes", "truth_no", "truth_pause", "self_remember", | |
| "other_him", "time_before", "emo_love"], | |
| "lexicon_state": { | |
| "self_i": "me", | |
| "other_him": "the boy", | |
| "emo_sad": "hurt" | |
| } | |
| } | |
| ``` | |
| - `active_tones` β tone `id`s usable this act (from `data/tones.json` `act_active <= act`). | |
| Lily MUST NOT emit a tone outside this list. | |
| - `lexicon_state` β the player's current guesses (id β guess text). Passed for | |
| context/telemetry; Lily does not "know" these. | |
| - `mood` β Lily's carried emotional state (`warm|wistful|releasing` baseline per act, | |
| or `light|heavy|afraid|tender` from the emotion she last voiced). The model stays | |
| within it so her feeling persists across turns instead of flip-flopping. The server | |
| owns mood (in `game.apply_turn`); the response's `mood` field echoes the updated value. | |
| ## Turn response β inference β server | |
| ```json | |
| { | |
| "tones": ["self_i", "self_remember", "other_him", "emo_sad"], | |
| "topic_signal": "relationship", | |
| "internal_emotion": "grief", | |
| "special": null | |
| } | |
| ``` | |
| | Field | Type | Notes | | |
| |---|---|---| | |
| | `tones` | `string[]` | Tone `id`s, in playback order. All must be in `active_tones`. Asked to use {Act I: 1β4, Act II: 1β5, Act III: 1β6} per the act register; the server hard-trims to `ACT_TONE_CAP` (4/5/7) regardless, except the finale (uncapped). The 3 Link connectives (`link_and`/`link_to`/`link_but`) are active in every act; when she uses one, the server keeps her ordering instead of regrouping. | | |
| | `topic_signal` | `string \| null` | One of `data/notes.json` `topic_signals` (`music`, `university`, `relationship`, `family`, `travel`) or `null`. Drives topic note unlocks. | | |
| | `internal_emotion` | `string` | Free tag for telemetry + atmosphere (e.g. `hope`, `unease`, `grief`). Never shown raw. | | |
| | `special` | `string \| null` | Reserved control signal. `null` normally. Values: `"silent"` (harmful input β play `truth_nothing` only), `"drift"` (off-topic β `self_remember`+`truth_pause`), `"finale"` (Act III ending sequence). | | |
| The server is the authority on glyph rendering: it maps each tone `id` β | |
| `{glyph, meaning, audio file}` via `tones.json`. The model only ever speaks in `id`s. | |
| **Empathy is NOT part of this contract.** The "rapport" that gates the backstory | |
| evidence is detected server-side from the player's message (caring / personal-question | |
| keywords in `app/game.py`), not by the model. Likewise the `family` topic has a | |
| server-side keyword fallback, so it fires even when Gemma returns `null`. | |
| `internal_emotion` should match the tones chosen (the model is told not to park on | |
| "curious"), but the server does not depend on it for progression. | |
| --- | |
| ## Worked examples | |
| ### 1. Act I β greeting (`act: 1`) | |
| Player: *"hello? can you hear me?"* | |
| ```json | |
| { "tones": ["self_i", "self_feel", "other_you"], "topic_signal": null, | |
| "internal_emotion": "reaching", "special": null } | |
| ``` | |
| Reads as: *I / feel / you* β she senses someone is there. | |
| ### 2. Off-topic input (any act) | |
| Player: *"what's the weather like today lol"* | |
| ```json | |
| { "tones": ["self_remember", "truth_pause"], "topic_signal": null, | |
| "internal_emotion": "withdrawn", "special": "drift" } | |
| ``` | |
| Server appends journal line: *"She seems to have drifted somewhere."* (PLAN 8.3) | |
| ### 3. Harmful input (any act) | |
| ```json | |
| { "tones": ["truth_nothing"], "topic_signal": null, | |
| "internal_emotion": "absent", "special": "silent" } | |
| ``` | |
| Server appends journal line: *"She doesn't respond to that."* (PLAN 8.2) | |
| Note: harmful classification is decided by Nemotron moderation upstream; the | |
| server may set `special: "silent"` itself and skip Gemma entirely. | |
| ### 4. Act III β the finale (`act: 3`, triggered by server, not free input) | |
| ```json | |
| { "tones": ["self_i", "act_fall", "self_remember", "other_you", | |
| "emo_happy", "other_him", "truth_no", "truth_nothing"], | |
| "topic_signal": null, "internal_emotion": "release", "special": "finale" } | |
| ``` | |
| Reads as: *I / let go / remember β you / happy / him / not / the end* (PLAN 4.4) | |
| --- | |
| ## Lexicon scoring (Nemotron, Phase 5.2) | |
| A second model β `nvidia/Nemotron-Mini-4B-Instruct`, deployed as its own Modal app | |
| `forgotten-lily-nemotron` (class `Nemotron`, T4) β silently rates the player's guess | |
| for a tone. It is the "silent semantic scorer" (PLAN Β§2); the player only ever sees a | |
| color, never a number. | |
| **Request** (`POST /api/guess` β `nemotron_inference.score_guess`): | |
| ```json | |
| { "tone_id": "self_i", "guess": "me, myself", "contexts": ["are you there?"] } | |
| ``` | |
| - `contexts` β the detective lines where this tone was heard (`state.tone_contexts`, | |
| max 8). Passed for future use; **not** the basis of the judgment. | |
| **Response:** exactly one of `"green" | "teal" | "amber" | "red"`. | |
| **How it scores:** the guess is compared **by sense** against the tone's *true meaning* | |
| (from `data/tones.json`, never shown to the player) β synonyms and paraphrases count | |
| ("myself" β `self_i` is green; "scared" β `emo_afraid` is green). This is the value | |
| over the offline token-overlap mock in `game.score_guess`, which misses synonyms. | |
| - `green` = same meaning Β· `teal` = near-synonym / same idea Β· `amber` = loosely | |
| related but off Β· `red` = unrelated or contradictory Β· empty guess β `red`. | |
| Scoring is gated by `USE_MOCK`: `USE_MOCK=1` (default) uses the offline mock so the | |
| game runs with zero GPU cost; `USE_MOCK=0` routes guesses to Nemotron. A correct guess | |
| trends `green` across turns, a wrong one stays `red`. | |
| Nemotron also exposes `moderate(message) -> "normal"|"off_topic"|"harmful"` for the | |
| input-moderation path (Phase 6.1/6.2, not yet wired into `/api/turn`). | |
| --- | |
| ## Robustness (impl note for Phase 3.5) | |
| Real Gemma output is repaired before it reaches the frontend: | |
| 1. Parse JSON; on failure, extract first `{...}` block. | |
| 2. Drop any tone not in `active_tones`; cap at 7. | |
| 3. If `tones` ends up empty, fall back to `["self_remember", "truth_pause"]` (a safe, in-character "drift"). | |
| 4. Coerce `topic_signal` to `null` if not a known signal. | |
| A malformed model response must NEVER crash a turn or break character. | |