decida / app.py
helmo's picture
Fix Spam filter wording: unsolicited MAIL or a scam
f623cda verified
Raw History Blame Contribute Delete
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)
@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()