"""SanSi typed-decision demo — one decision, read after every thinking loop. SanSi (minnesotanlp/SanSi-2.6B) turns a language model into a classifier over *declared* options: it never generates text, it returns a probability per option after each of the Ouro backbone's loops. One forward pass therefore answers at every compute budget 1-8. """ import html import time import gradio as gr import spaces # must precede any torch/CUDA-touching import import torch from sansi.hub import decide, load MODEL_ID = "minnesotanlp/SanSi-2.6B" model, tok = load(MODEL_ID) # Ouro-2.6B + LoRA + readout, moved to cuda by load() model.to("cuda") # --------------------------------------------------------------------------- UI # Validated categorical palette, slot 1, both modes (dataviz reference instance). SERIES_LIGHT = "#2a78d6" SERIES_DARK = "#3987e5" GREEN_LIGHT = "#006300" # success text token, light surface GREEN_DARK = "#0ca30c" RED_LIGHT = "#d03b3b" # status critical, light surface (icon+label paired in UI) RED_DARK = "#e66767" CSS = """ .dark .gradio-container { color: var(--body-text-color); } :root .prob-fill { background: SERIES_LIGHT_HEX; } :root:where([data-theme="dark"]) .prob-fill, :root[data-theme="dark"] .prob-fill { background: SERIES_DARK_HEX; } :root .strip-step { background: rgba(42,120,214,.14); } :root:where([data-theme="dark"]) .strip-step, :root[data-theme="dark"] .strip-step { background: rgba(57,135,229,.22); } .sansi-wrap { font-variant-numeric: tabular-nums; } .sansi-row { display: flex; align-items: center; gap: 10px; margin: 6px 0; } .sansi-letter { flex: 0 0 26px; font-weight: 700; } .sansi-opttext { flex: 1 1 auto; min-width: 0; } .sansi-track { flex: 0 0 100px; display: inline-block; } .sansi-p { flex: 0 0 48px; text-align: right; } .prob-track { background: var(--panel-background-color, transparent); border: 1px solid var(--border-color-primary, rgba(11,11,11,.10)); border-radius: 6px; height: 26px; position: relative; overflow: hidden; display: block; } .prob-fill { height: 100%; border-radius: 5px 2px 2px 5px; } .verdict { font-size: 1.02em; margin: 10px 0 2px; } .strip { display: flex; gap: 3px; margin: 6px 0 14px; } .strip-step { flex: 1 1 0; text-align: center; border-radius: 4px; padding: 4px 2px; font-size: 0.9em; } """ # --------------------------------------------------------------------- examples # The authors' own showcase items (Sansi/docs/data/items.json, Apache-2.0) + the # model-card quick start. p values listed are from the paper (SanSi-1.4B analysis # model) — the demo serves SanSi-2.6B, so numbers differ slightly. EXAMPLES = [ # label, state, question, options, loops ["Shortest of three (model card)", "Mia is taller than Sam. Sam is taller than Lee.", "Who is the shortest?", "Mia\nSam\nLee", 8], ["Paraphrase: answer flips with thought (paper)", "The six people killed were four Burmese citizens and two Russians .", 'Does this sentence mean the same thing: "The six people killed were four Russian and two Burmese citizens ."', "no: Different meaning, even if most words match\nyes: Same meaning, possibly reworded", 8], ["Logic: entailment flips to unknown (paper)", "No road is dustless. Some streets are roads.", 'Using only the facts and rules above, is the statement "Some streets are dustless." true, false, or unknown?', "true\nfalse\nunknown", 8], ["Math: settles on 300 after 8 loops (paper)", '{\n "subject": "elementary mathematics",\n "question": "If 30,000 is divided by 10 and then divided by 10 again, what will be the resulting number?"\n}', "Which option correctly answers the question?", "a: 3\nb: 30\nc: 300\nd: 3,000", 8], ["MNLI: contradiction, not entailment (teaser)", "I guess I lay there unconscious awhile.", 'Given the text above, is this hypothesis entailed, neutral, or contradicted? Hypothesis: "I lay awake for a long time."', "entailment\nneutral\ncontradiction", 8], ["Unanswerable: stays near-uniform (SQuAD 2.0)", "Fresno is the largest U.S. city not directly linked to an Interstate highway. When the Interstate Highway System was created in the 1950s, the decision was made to build what is now Interstate 5 on the west side of the Central Valley, and thus bypass many of the population centers in the region, instead of upgrading what is now State Route 99. Due to rapidly raising population and traffic in cities along SR 99, as well as the desirability of Federal funding, much discussion has been made to upgrade it to interstate standards and eventually incorporate it into the interstate system, most likely as Interstate 9. Major improvements to signage, lane width, median separation, vertical clearance, and other concerns are currently underway.", "What do many people want to integrate into interstate 5? (Answer from the passage above.)", "1950s\nFresno\nwest\nSR 99", 8], ] @spaces.GPU(duration=25) def decide_all(state, question, options_text, loops): """Option probabilities after every loop. Args: state: context the decision is grounded in ("" allowed). question: the question to answer. options_text: one option per line, 2 to 26 of them. loops: how many thinking loops to run (1-16; >8 reuse the last trained readout). Returns: (report_html, last_probs_json, elapsed_seconds) """ t0 = time.perf_counter() options = [o.strip() for o in options_text.split("\n") if o.strip()] if not 2 <= len(options) <= 26: raise gr.Error(f"Give between 2 and 26 options (one per line) — got {len(options)}.") probs_per_loop = decide(model, tok, state.strip(), question.strip(), options, loops=int(loops)) elapsed = time.perf_counter() - t0 n = len(options) letters = [chr(ord("A") + i) for i in range(n)] final = probs_per_loop[-1] arg = max(range(n), key=final.__getitem__) p_max = final[arg] threshold = (1 + 1 / n) / 2 # "hard answer" cut from the paper confident = p_max >= threshold esc = [html.escape(o) for o in options] rows = [] for i in range(n): pct = final[i] * 100 fill = f'
' rows.append( f'{verdict}
' + "".join(rows) + f'Declare the options, get a probability per option — read after every thinking loop. No text is generated.