import os os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") import spaces # must be imported before torch import html import json import math import sys import time from pathlib import Path import gradio as gr import torch from huggingface_hub import snapshot_download MODEL_ID = "nace-ai/drex-v1.5" # The checkpoint ships its runtime with the weights: code/kev (tokenizer-to-answer stack, Apache-2.0), # a Qwen3.5 hybrid backbone (bf16 safetensors) and head.pt (pointer head). CKPT = snapshot_download( MODEL_ID, allow_patterns=[ "*.json", "*.jinja", "inference.py", "head.pt", "*.safetensors", "code/*", "code/**/*", ], ) sys.path.insert(0, str(Path(CKPT) / "code")) from kev.api import SystemOneRequest, output_tokens, to_answers, to_record # noqa: E402 from kev.checkpoint import load # noqa: E402 from kev.model import ContextOverflow, SERVE_MAX_BRANCH, SERVE_MAX_STATE # noqa: E402 # checkpoint.load() defaults to CUDA when available and bf16 there (fp32 otherwise); # .to("cuda") inside DecisionModel is intercepted by the ZeroGPU hijack and packed at startup. tok, model = load(CKPT) print(f"Loaded {MODEL_ID} on {model.device} ({model.dtype}); hybrid={model.hybrid}", flush=True) MAX_STATE_CHARS = 40000 EX_REQUEST = { "state": {"ticket": "I was charged twice for the same order. Please refund the extra payment."}, "questions": { "team": { "type": "choice", "instructions": "Which team should handle this ticket?", "criteria": {"billing": "Payments, charges, and refunds", "technical": "Bugs and outages", "other": "Anything else"}, }, "refund": {"type": "noul", "instructions": "Does the customer explicitly ask for a refund?"}, "urgency": {"type": "score", "instructions": "How urgent is this ticket?", "criteria": ["Routine", "Soon", "Urgent"]}, }, } EX_SERVICE = { "state": {"alert": "checkout-api error_rate jumped from 0.2% to 34% five minutes ago; latency_p99 is 4.8 s and orders are failing."}, "questions": { "sev": { "type": "score", "instructions": "How severe is this incident?", "criteria": ["Minor", "Elevated", "Major", "Critical"], }, "page": {"type": "noul", "instructions": "Should the on-call engineer be paged right now?"}, "area": { "type": "choice", "instructions": "Which area is most likely involved?", "criteria": { "payments": "Charging, refunds, invoices", "frontend": "Checkout UI and browser errors", "backend": "API failures and latency", "infra": "Deployments, scaling, cloud resources", }, }, }, } EX_REVIEW = { "state": {"review": "The room was spotless and the staff were wonderful, but the room heater rattled all night and I barely slept.", "stars": 2}, "questions": { "sentiment": { "type": "score", "instructions": "How positive is this review overall?", "criteria": ["Very negative", "Negative", "Neutral", "Positive", "Very positive"], }, "would_return": {"type": "noul", "instructions": "Does the reviewer sound likely to return?"}, "followup": { "type": "choice", "instructions": "What should the hotel do first?", "criteria": { "maintenance": "Fix the heater", "compensation": "Offer a refund or discount", "thanks": "Thank the reviewer", "nothing": "No action needed", }, }, }, } def _dump(value) -> str: return value if isinstance(value, str) else json.dumps(value, indent=2, ensure_ascii=False) def _first_error(error) -> str: """Pydantic wraps every failing question in one long message; keep the head of it.""" text = str(error).replace("\n", " ") return text[:300] + ("…" if len(text) > 300 else "") def _parse_state(state: str): """Plain text passes through; JSON-looking text is parsed into a structured state.""" text = (state or "").strip() if not text: raise gr.Error("Please provide a state (plain text or JSON).") if text[0] in "{[": try: return json.loads(text) except json.JSONDecodeError: pass return text def _gpu_duration(state: str, questions_json: str, *args, **kwargs) -> int: # Measured on ZeroGPU: 87-token sample ~3.6 s cold / ~0.1 s warm; 8k-token state with # 3 questions ~4.7 s model time. Scale with input size, capped well under the tier limit. chars = len(state or "") + len(questions_json or "") return int(min(60, 10 + chars / 3000)) @spaces.GPU(duration=_gpu_duration) def decide(state: str, questions_json: str): """Answer typed questions about a document with Drex v1.5. Args: state: The document to decide about. Plain text, or JSON (object/list) which is rendered as indented text for the model. questions_json: JSON object mapping question names to questions. Each question has a `type` — `choice` (named options), `noul` (yes/no) or `score` (ordered levels) — optional `instructions`, and `criteria` (object of option → description for choice, list of level descriptions for score, optional true/false descriptions for noul). Returns: Per-question probability distributions rendered as bars, plus the SystemOne JSON response (answer, confidence and probability per option for every question). """ parsed_state = _parse_state(state) try: parsed_questions = json.loads(questions_json or "") except json.JSONDecodeError as error: raise gr.Error(f"Questions must be valid JSON: {error}") if not isinstance(parsed_questions, dict) or not parsed_questions: raise gr.Error("Questions must be a non-empty JSON object keyed by question name.") request = {"state": parsed_state, "questions": parsed_questions} if len(json.dumps(parsed_state, ensure_ascii=False)) > MAX_STATE_CHARS: raise gr.Error(f"State is too large (>{MAX_STATE_CHARS} characters). Use a shorter document.") try: req = SystemOneRequest.model_validate(request) except Exception as error: raise gr.Error(f"Invalid request: {_first_error(error)}") try: rec, meta = to_record(req) enc = model.encode(tok, rec, max_state=SERVE_MAX_STATE, max_branch=SERVE_MAX_BRANCH, strict=True) except ContextOverflow as error: raise gr.Error(f"Too long for the context window: {error}") torch.cuda.synchronize() started = time.perf_counter() with torch.no_grad(): probs = [p.float().tolist() for p in model.probs(enc)] torch.cuda.synchronize() latency_ms = (time.perf_counter() - started) * 1000 if len(probs) != len(meta) or any( len(row) != len(question["keys"]) or any(not math.isfinite(value) for value in row) for row, question in zip(probs, meta) ): raise gr.Error("Model readout is incomplete or non-finite; try a shorter request.") answers = to_answers(probs, meta) response = { "model": "drex-v1.5", "answers": answers, "usage": { "input_tokens": len(enc["ids"]), "output_tokens": output_tokens(tok, answers), }, "latency_ms": round(latency_ms, 1), } return _render(parsed_questions, answers, latency_ms, len(enc["ids"])), response def _render(questions: dict, answers: dict, latency_ms: float, n_tokens: int) -> str: cards = [] for qid, question in questions.items(): ans = answers[qid] if ans["type"] == "noul": options = [("yes", "yes", ans["noul"]), ("no", "no", 1.0 - ans["noul"])] elif ans["type"] == "choice": criteria = question.get("criteria") or {} options = [ (k, f"{k} — {v}" if isinstance(v, str) and v else k, ans["probabilities"][k]) for k, v in criteria.items() ] else: # score legend = ans["legend"] options = [(f"{i}: {legend[i]}", legend[i], ans["probabilities"][str(i)]) for i in range(len(legend))] best = max(options, key=lambda o: o[2])[0] rows = [] for key, label, value in options: highlight = "drex-best" if key == best else "" rows.append( f'
{html.escape(str(label))}
' f'
' f'
{value * 100:.1f}%
' ) extra = "" if ans["type"] == "noul": extra = f' · P(yes) {ans["noul"] * 100:.1f}%' elif ans["type"] == "score": extra = f' · expected score {ans["score"]:.2f} of {len(ans["legend"]) - 1}' if "confidence" in ans: extra += f' · confidence {ans["confidence"] * 100:.1f}%' instr = question.get("instructions") or qid cards.append( f'
{html.escape(str(qid))}' f'{ans["type"]}
' f'
{html.escape(str(instr))}
' f'
→ {html.escape(str(best))}{extra}
{"".join(rows)}
' ) footer = f'
{n_tokens} input tokens · model time {latency_ms:.0f} ms · no text generated
' return '
' + "".join(cards) + footer + "
" CSS = """ #col-container { max-width: 1100px; margin: 0 auto; } .dark .gradio-container { color: var(--body-text-color); } .drex-wrap { display: flex; flex-direction: column; gap: 12px; } .drex-card { border: 1px solid var(--border-color-primary); border-radius: 10px; padding: 12px 14px; } .drex-head { display: flex; gap: 8px; align-items: center; margin-bottom: 2px; } .drex-id { font-weight: 700; font-family: var(--font-mono); } .drex-type { font-size: 0.75em; padding: 1px 8px; border-radius: 999px; background: var(--color-accent-soft); } .drex-instr { opacity: 0.8; font-size: 0.92em; } .drex-answer { margin: 6px 0 8px; } .drex-row { display: grid; grid-template-columns: minmax(120px, 40%) 1fr 56px; gap: 8px; align-items: center; margin: 3px 0; font-size: 0.9em; } .drex-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .drex-bar { height: 12px; border-radius: 6px; background: var(--block-background-fill); border: 1px solid var(--border-color-primary); overflow: hidden; } .drex-fill { height: 100%; background: var(--neutral-400); } .drex-best .drex-fill { background: var(--color-accent); } .drex-best .drex-label { font-weight: 600; } .drex-val { text-align: right; font-family: var(--font-mono); } .drex-foot { font-size: 0.8em; opacity: 0.7; } """ with gr.Blocks(title="Drex v1.5") as demo: with gr.Column(elem_id="col-container"): gr.Markdown( "# Drex v1.5 — typed decisions with probabilities\n" "[nace-ai/drex-v1.5](https://huggingface.co/nace-ai/drex-v1.5) is a 9B **decision model** from " "[Nace.AI](https://www.nace.ai/): give it a **state** (a document: text or JSON) and named typed questions — " "`choice` (pick an option), `noul` (yes/no) or `score` (ordered levels) — and it returns a " "probability for every option, one forward pass per question. No text is generated. " "Built on [MiMo-V2.6-Distill-Qwen-9B](https://huggingface.co/XiaomiMiMo/MiMo-V2.6-Distill-Qwen-9B) " "(Qwen3.5 hybrid backbone) with a pointer-head readout." ) with gr.Row(): with gr.Column(scale=1): state = gr.Textbox( label="State (the document: plain text or JSON)", lines=5, placeholder="Paste a support ticket, a log line, a JSON record, a contract clause…", ) questions = gr.Code( label="Questions (JSON: name → {type, instructions, criteria})", language="json", value=_dump(EX_REQUEST["questions"]), lines=16, ) run = gr.Button("Decide", variant="primary") with gr.Column(scale=1): result_html = gr.HTML(label="Decisions") with gr.Accordion("SystemOne JSON response", open=False): result_json = gr.JSON(label="Response") run.click( decide, inputs=[state, questions], outputs=[result_html, result_json], api_name="decide", ) gr.Examples( label="Examples", examples=[ [_dump(EX_REQUEST["state"]), _dump(EX_REQUEST["questions"])], [_dump(EX_SERVICE["state"]), _dump(EX_SERVICE["questions"])], [_dump(EX_REVIEW["state"]), _dump(EX_REVIEW["questions"])], ], inputs=[state, questions], outputs=[result_html, result_json], fn=decide, cache_examples=True, cache_mode="lazy", ) if __name__ == "__main__": demo.launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)