File size: 11,729 Bytes
a2a5f1d 8387116 a2a5f1d 8387116 f623cda 8387116 a2a5f1d 8387116 33e686f 8387116 a2a5f1d 8387116 a2a5f1d 8387116 a2a5f1d 8387116 33e686f 8387116 33e686f 8387116 a2a5f1d 8387116 a2a5f1d 8387116 a2a5f1d 8387116 a2a5f1d 8387116 a2a5f1d 8387116 a2a5f1d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | """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)
@GPU
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()
|