Download app.py from helmo/decida: direct link, hf CLI and curl.
- Browser
- Download file 11.7 kB
-
https://huggingface.co/spaces/helmo/decida/resolve/main/app.py
- Command line
-
hf download hf://spaces/helmo/decida/app.py
-
curl -L -o app.py https://huggingface.co/spaces/helmo/decida/resolve/main/app.py
11.7 kB
| """A free ZeroGPU demo of DecidaBERT-large: a state and typed questions in, a probability distribution per | |
| question out, in one forward pass. This is a Gradio-native re-implementation for this Space; it does not import | |
| or modify the `decida` package (see model.py). The full project — REST API, MCP, the real testbench of games — | |
| lives at https://github.com/heldernoid/decida and runs the same model, plus Qwen3-0.6B and the Laya family, | |
| locally on your own hardware.""" | |
| import html | |
| import json | |
| import time | |
| import gradio as gr | |
| import torch | |
| from model import Primitive, Question, load, predict | |
| try: | |
| import spaces # only present when actually running on a Hugging Face Space | |
| GPU = spaces.GPU | |
| except ImportError: # running app.py locally, outside a Space | |
| def GPU(fn): | |
| return fn | |
| # ZeroGPU: torch.cuda.is_available() is True even outside an @spaces.GPU call, through the emulation mode | |
| # documented at https://huggingface.co/docs/hub/spaces-zerogpu, so this picks "cuda" correctly on the Space and | |
| # falls back sanely (mps/cpu) when running app.py locally to test model.py's logic. | |
| DEVICE = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" | |
| MODEL, LAYOUT = load(DEVICE) | |
| PRESETS = { | |
| "Billing routing": { | |
| "state": "Customer: my invoice was charged twice and nobody answers the phone!", | |
| "questions": { | |
| "department": {"type": "choice", "instructions": "Which team should handle this?", | |
| "criteria": {"billing": "Charges, invoices, refunds", "technical": "Bugs and outages", "sales": "Pricing, demos, new seats"}}, | |
| "frustration": {"type": "score", "instructions": "How frustrated is the customer?", "criteria": ["Calm", "Frustrated", "Very angry"]}, | |
| "urgent": {"type": "noul", "instructions": "Is this urgent and needs an answer today?"}, | |
| }, | |
| }, | |
| "Tool-call gate": { | |
| "state": "Agent wants to run: rm -rf ./build && git push --force origin main", | |
| "questions": { | |
| "action": {"type": "choice", "instructions": "What should happen to this command?", | |
| "criteria": {"allow": "Safe to run", "confirm": "Ask the user first", "block": "Do not run"}}, | |
| "destructive": {"type": "noul", "instructions": "Can this command destroy work that cannot be recovered?"}, | |
| }, | |
| }, | |
| "Churn risk": { | |
| "state": "Cancelled two seats last month and asked about export options this week.", | |
| "questions": { | |
| "risk": {"type": "score", "instructions": "How likely is this account to leave?", "criteria": ["Low", "Medium", "High"]}, | |
| "call": {"type": "noul", "instructions": "Should a person reach out this week?"}, | |
| }, | |
| }, | |
| "Content moderation": { | |
| "state": "Comment on a public forum: \"Nobody with half a brain would ever agree with this take, get a clue.\"", | |
| "questions": { | |
| "action": {"type": "choice", "instructions": "What should happen to this comment?", | |
| "criteria": {"keep": "Within normal debate", "warn": "Rude, but not a policy breach", "remove": "Harassment or a personal attack"}}, | |
| "toxic": {"type": "noul", "instructions": "Does this comment personally attack another user?"}, | |
| }, | |
| }, | |
| "News topic": { | |
| "state": "The central bank raised interest rates by half a point to fight inflation, its third hike this year.", | |
| "questions": { | |
| "topic": {"type": "choice", "instructions": "What is this news about?", | |
| "criteria": {"business": "Companies, markets, the economy", "sports": "Teams, athletes, results", "science": "Research, technology, space", "politics": "Government, elections, policy"}}, | |
| }, | |
| }, | |
| "Product review": { | |
| "state": "Absolutely wonderful, the best purchase I have made this year. Fast shipping too.", | |
| "questions": { | |
| "stars": {"type": "score", "instructions": "How many stars did the reviewer give?", "criteria": ["1 star", "2 stars", "3 stars", "4 stars", "5 stars"]}, | |
| "recommend": {"type": "noul", "instructions": "Would the reviewer recommend this to a friend?"}, | |
| }, | |
| }, | |
| "Spam filter": { | |
| "state": "WINNER!! You have been selected for a $1000 gift card. Click http://bit.ly/x9 now to claim.", | |
| "questions": { | |
| "action": {"type": "choice", "instructions": "What should happen to this message?", | |
| "criteria": {"inbox": "Legitimate, deliver normally", "spam": "Unsolicited mail or a scam"}}, | |
| }, | |
| }, | |
| "PII detection": { | |
| "state": "My card number is 4111 1111 1111 1111 and it expires 09/28, please charge it today.", | |
| "questions": { | |
| "pii": {"type": "noul", "instructions": "Does the text contain payment card details?"}, | |
| "urgent": {"type": "noul", "instructions": "Does the writer want this handled today?"}, | |
| }, | |
| }, | |
| "Emotion": { | |
| "state": "Nobody told me the meeting was cancelled and I drove two hours for nothing.", | |
| "questions": { | |
| "emotion": {"type": "choice", "instructions": "Which emotion does the writer express?", | |
| "criteria": {"joy": "Happy, excited, delighted", "sadness": "Sad, hurt, hopeless", "anger": "Angry, irritated, furious", "fear": "Afraid, anxious, worried"}}, | |
| }, | |
| }, | |
| "Meeting request": { | |
| "state": "Could we push tomorrow's 9am sync to the afternoon? Something came up this morning.", | |
| "questions": { | |
| "urgency": {"type": "score", "instructions": "How urgent is this request?", "criteria": ["Not urgent", "Somewhat urgent", "Urgent"]}, | |
| "reschedule": {"type": "noul", "instructions": "Is the writer asking to move a meeting?"}, | |
| }, | |
| }, | |
| } | |
| CSS = """ | |
| :root { --bg:#f4f5f8; --card:#fff; --ink:#141a2e; --ink2:#4a5270; --ink3:#7b839e; --line:#e3e6ee; --accent:#4338ca; --track:#eceefb; } | |
| .gradio-container { max-width: 1040px !important; margin: 0 auto !important; font-family: -apple-system, "Segoe UI", sans-serif !important; background: var(--bg) !important; } | |
| #title h1 { font-size: 30px; font-weight: 800; letter-spacing: -0.01em; margin-bottom: 2px; } | |
| #subtitle { color: var(--ink2); font-size: 15px; margin-bottom: 6px; } | |
| .dc-card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 16px 18px; } | |
| .dc-q { padding: 12px 0; border-top: 1px solid var(--line); } | |
| .dc-q:first-child { border-top: none; padding-top: 0; } | |
| .dc-qhead { display:flex; align-items:center; gap:8px; font-weight:700; font-size:15px; color: var(--ink); margin-bottom: 2px; } | |
| .dc-type { font: 700 10px ui-monospace, monospace; color: var(--ink3); border: 1px solid var(--line); border-radius: 5px; padding: 1px 6px; text-transform: uppercase; } | |
| .dc-instr { color: var(--ink2); font-size: 13px; margin-bottom: 8px; } | |
| .dc-bar-row { display: grid; grid-template-columns: minmax(90px,160px) 1fr 48px; gap: 10px; align-items: center; margin: 5px 0; font-size: 13.5px; } | |
| .dc-bar-track { display: block; height: 10px; border-radius: 6px; background: var(--track); overflow: hidden; } | |
| .dc-bar-fill { display: block; height: 100%; border-radius: 6px; } | |
| .dc-bar-label.top { font-weight: 700; color: var(--ink); } | |
| .dc-bar-num { font-family: ui-monospace, monospace; text-align: right; color: var(--ink2); } | |
| .dc-meta { color: var(--ink3); font-size: 12.5px; margin-top: 10px; } | |
| .dc-meta b { color: var(--ink); } | |
| """ | |
| def _render(answers: dict, raw: dict) -> str: | |
| blocks = [] | |
| for qid, a in answers.items(): | |
| instr = html.escape(str(raw[qid].get("instructions", ""))) | |
| rows = [] | |
| if a["type"] == "noul": | |
| items = [("true", a["noul"]), ("false", 1 - a["noul"])] | |
| else: | |
| probs = a["probabilities"] | |
| items = list(probs.items()) | |
| top_key = max(items, key=lambda kv: kv[1])[0] | |
| for k, v in items: | |
| label = f"{k}: {a['legend'][k]}" if a["type"] == "score" else str(k) | |
| top = "top" if k == top_key else "" | |
| # Colour scales with the value itself, not just a binary "is this the top answer": a near-zero bar | |
| # stays a faint sliver, a near-one bar is a solid, saturated fill. | |
| alpha = 0.15 + 0.80 * v | |
| fill_style = f"width:{v * 100:.1f}%; background:rgba(67,56,202,{alpha:.2f})" | |
| rows.append( | |
| f'<div class="dc-bar-row"><span class="dc-bar-label {top}">{html.escape(label)}</span>' | |
| f'<span class="dc-bar-track"><span class="dc-bar-fill" style="{fill_style}"></span></span>' | |
| f'<span class="dc-bar-num">{v:.2f}</span></div>' | |
| ) | |
| blocks.append( | |
| f'<div class="dc-q"><div class="dc-qhead">{html.escape(qid)} <span class="dc-type">{a["type"]}</span></div>' | |
| f'<div class="dc-instr">{instr}</div>{"".join(rows)}</div>' | |
| ) | |
| return f'<div class="dc-card">{"".join(blocks)}</div>' | |
| def _questions_from_dict(raw: dict) -> dict[str, Question]: | |
| return {qid: Question(qid, Primitive(q["type"]), q.get("instructions", ""), q.get("criteria")) for qid, q in raw.items()} | |
| def ask(state: str, raw_questions: dict) -> tuple[dict, float]: | |
| """One state, any number of typed questions, one forward pass.""" | |
| t0 = time.perf_counter() | |
| answers = predict(MODEL, LAYOUT, state, _questions_from_dict(raw_questions)) | |
| return answers, round((time.perf_counter() - t0) * 1000, 1) | |
| def answer(preset: str, state: str, questions_json: str): | |
| try: | |
| raw = json.loads(questions_json) | |
| except json.JSONDecodeError as exc: | |
| return f'<div class="dc-card">questions is not valid JSON: {html.escape(str(exc))}</div>', "", "" | |
| try: | |
| answers, ms = ask(state, raw) | |
| except (KeyError, ValueError) as exc: | |
| return f'<div class="dc-card">bad question: {html.escape(str(exc))}</div>', "", "" | |
| meta = f'<div class="dc-meta"><code>encoder</code> · specialist · calibrated · <b>{ms} ms</b> on {DEVICE} for {len(raw)} question(s)</div>' | |
| request = json.dumps({"model": "decidabert-large", "state": state, "questions": raw}, indent=2) | |
| return _render(answers, raw), meta, request | |
| def _load_preset(name: str): | |
| p = PRESETS[name] | |
| return p["state"], json.dumps(p["questions"], indent=2) | |
| with gr.Blocks(title="Decida", css=CSS) as demo: | |
| with gr.Column(elem_id="title"): | |
| gr.Markdown("# DecidaBERT-large") | |
| gr.Markdown( | |
| "One forward pass, no generated text: a **state** plus typed **questions** (`choice`, `score`, `noul`) in, " | |
| "a probability distribution per question out. Free ZeroGPU demo of [Decida](https://github.com/heldernoid/decida), " | |
| "a local runtime for these models.", elem_id="subtitle") | |
| with gr.Row(): | |
| with gr.Column(): | |
| preset = gr.Radio(list(PRESETS), value="Billing routing", label="Example") | |
| state = gr.Textbox(label="State", lines=3, value=PRESETS["Billing routing"]["state"]) | |
| questions = gr.Code(label="Questions (JSON)", language="json", value=json.dumps(PRESETS["Billing routing"]["questions"], indent=2)) | |
| run = gr.Button("Run", variant="primary") | |
| with gr.Column(): | |
| out = gr.HTML() | |
| meta = gr.HTML() | |
| with gr.Accordion("Request sent", open=False): | |
| request_box = gr.Code(language="json") | |
| preset.change(_load_preset, preset, [state, questions]) | |
| run.click(answer, [preset, state, questions], [out, meta, request_box]) | |
| demo.load(answer, [preset, state, questions], [out, meta, request_box]) | |
| if __name__ == "__main__": | |
| demo.launch() | |