Spaces:
Running on Zero
Running on Zero
Fix multi-turn chat: lock composer during generation, always stream into a fresh end-of-history bubble
af1e438 verified | import os | |
| import re | |
| from collections.abc import Iterator | |
| from pathlib import Path | |
| from threading import Thread | |
| PREVIEW = os.getenv("TWIL_UI_PREVIEW") == "1" | |
| if not PREVIEW: | |
| import spaces | |
| import torch | |
| from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer | |
| else: | |
| class spaces: # type: ignore[no-redef] | |
| def GPU(duration=None, **kwargs): | |
| def decorator(fn): | |
| return fn | |
| return decorator | |
| import gradio as gr | |
| MODEL_ID = "webAI-Official/TwIL-LM3" | |
| MAX_INPUT_TOKEN_LENGTH = int(os.getenv("MAX_INPUT_TOKEN_LENGTH", "8192")) | |
| ASSETS = Path(__file__).resolve().parent / "brand" | |
| AVATAR = ASSETS / "avatar.svg" | |
| THINK_OPEN = chr(60) + "think" + chr(62) | |
| THINK_CLOSE = chr(60) + chr(47) + "think" + chr(62) | |
| _THINK_RE = re.compile( | |
| re.escape(THINK_OPEN) + r".*?(" + re.escape(THINK_CLOSE) + r"|$)", | |
| re.DOTALL, | |
| ) | |
| SYSTEM_PROMPT = ( | |
| "You are TwIL, a formal-logic reasoning model created by webAI Intelligence Lab. " | |
| "You specialise in entailment, first-order logic, semantic parsing, Lean, and proof critique. " | |
| f"Work through the problem in a {THINK_OPEN} block, then give a concise, precise answer." | |
| ) | |
| CHIPS = [ | |
| ( | |
| "Analyze an argument", | |
| "Analyze this argument: identify the premises and the conclusion, say whether it is valid, and name any fallacy.\n\n", | |
| ), | |
| ( | |
| "Check an entailment", | |
| "Do these premises entail the conclusion? Give a formal proof or a counterexample.\n\nPremise 1: \nPremise 2: \nConclusion: ", | |
| ), | |
| ( | |
| "Translate to FOL", | |
| "Translate into first-order logic: Every student who studies hard passes at least one exam.", | |
| ), | |
| ] | |
| SHOWCASE_TEMPLATE = ( | |
| "Analyze this argument: extract the premises and the conclusion, translate them " | |
| "into first-order logic, state whether the argument is valid, and name any fallacy.\n\n" | |
| 'Argument: "{argument}"' | |
| ) | |
| SHOWCASE = [ | |
| { | |
| "kicker": "Viral tweet", | |
| "quote": "The streets are wet this morning, and rain always makes the streets wet. So it obviously rained last night.", | |
| "hint": "Can wet streets prove rain?", | |
| }, | |
| { | |
| "kicker": "School board speech", | |
| "quote": "If we let students retake this one exam, soon they will demand to retake every exam, and before long no grade at this school will mean anything.", | |
| "hint": "Does one retake doom every grade?", | |
| }, | |
| { | |
| "kicker": "Supplement ad", | |
| "quote": "Our formula is 100% natural, and nature knows best. That means it is completely safe for your body.", | |
| "hint": "Does natural imply safe?", | |
| }, | |
| { | |
| "kicker": "Campaign rally", | |
| "quote": "Either you support this bill or you don't care about public safety. You oppose the bill, so you don't care about public safety.", | |
| "hint": "Are those really the only options?", | |
| }, | |
| { | |
| "kicker": "Toothpaste commercial", | |
| "quote": "4 out of 5 dentists we surveyed recommend SparkleDent, and experts know best. You should switch to SparkleDent today.", | |
| "hint": "What is the survey hiding?", | |
| }, | |
| { | |
| "kicker": "HR policy memo", | |
| "quote": "Every employee who completes the security training receives a certificate. Sam completed the security training. Therefore Sam receives a certificate.", | |
| "hint": "One of these six actually holds up.", | |
| }, | |
| ] | |
| SHOWCASE_INTRO = """ | |
| <div class="twil-showcase-intro"> | |
| <h2>Can TwIL spot the flaw?</h2> | |
| <p>Six arguments from the wild. One click and TwIL extracts the premises, translates | |
| them to first-order logic, and rules on validity. Five are broken — one holds up.</p> | |
| </div> | |
| """ | |
| def _card_html(case: dict) -> str: | |
| return ( | |
| '<div class="twil-card-body">' | |
| f'<div class="twil-card-kicker">{case["kicker"]}</div>' | |
| f'<div class="twil-card-quote">“{case["quote"]}”</div>' | |
| f'<div class="twil-card-hint">{case["hint"]}</div>' | |
| "</div>" | |
| ) | |
| WAITLIST_URL = "https://www.webai.com/waitlist" | |
| HEADER = f""" | |
| <div class="twil-header"> | |
| <img class="twil-mark" src="/gradio_api/file=brand/AppiCon.svg" alt="webAI" /> | |
| <div class="twil-header-copy"> | |
| <div class="twil-wordmark">webAI</div> | |
| <div class="twil-product">TwIL-LM3 · Intelligence Lab</div> | |
| </div> | |
| <a class="twil-cta twil-cta-header" href="{WAITLIST_URL}" target="_blank" rel="noopener">Get early access</a> | |
| </div> | |
| """ | |
| HEAD = """ | |
| <script> | |
| document.addEventListener("keydown", (e) => { | |
| if (e.key !== "Enter" || e.shiftKey || e.isComposing) return; | |
| const ta = document.querySelector("#twil-input textarea"); | |
| if (!ta || e.target !== ta) return; | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| document.querySelector("#send-btn")?.click(); | |
| }, true); | |
| </script> | |
| """ | |
| PLACEHOLDER = f""" | |
| <div class="twil-empty"> | |
| <img class="twil-cube" src="/gradio_api/file=brand/webai-cube-256.webp" alt="" /> | |
| <h1>What should we work on?</h1> | |
| <p>TwIL-LM3 is webAI's formal-logic model — entailment, FOL, Lean, rule induction.</p> | |
| <div class="twil-waitlist"> | |
| <p>TwIL and the full family of webAI models are coming to the webAI app. | |
| Be the first to try expert intelligence on everyday devices.</p> | |
| <a class="twil-cta" href="{WAITLIST_URL}" target="_blank" rel="noopener">Join the waitlist</a> | |
| </div> | |
| </div> | |
| """ | |
| CSS = """ | |
| :root { | |
| --weba-canvas: #f4f4f4; | |
| --weba-fg: #161616; | |
| --weba-muted: #737373; | |
| --weba-secondary: #e8e8e8; | |
| --weba-border: rgba(22, 22, 22, 0.1); | |
| --weba-composer: #ffffff; | |
| --weba-primary: #232323; | |
| --weba-primary-fg: #fafafa; | |
| } | |
| .dark { | |
| --weba-canvas: #232323; | |
| --weba-fg: #fafafa; | |
| --weba-muted: #a8a8a8; | |
| --weba-secondary: #323232; | |
| --weba-border: rgba(255, 255, 255, 0.1); | |
| --weba-composer: #161616; | |
| --weba-primary: #e8e8e8; | |
| --weba-primary-fg: #232323; | |
| } | |
| * { box-shadow: none !important; text-shadow: none !important; } | |
| html, body, #root, .gradio-container, .gradio-container > .main, | |
| .gradio-container .contain, .fillable, | |
| .gradio-container .column, .gradio-container .row, .contain, .wrapper { | |
| background: var(--weba-canvas) !important; | |
| box-shadow: none !important; | |
| filter: none !important; | |
| } | |
| /* Never paint over content while a job is running: the status tracker | |
| overlay must stay transparent or the chat looks blank mid-generation. */ | |
| .gradio-container [data-testid="status-tracker"], | |
| .gradio-container .wrap.default { | |
| background: transparent !important; | |
| } | |
| html, body, .gradio-container, .gradio-container > .main, .fillable, | |
| .contain, .app { | |
| height: 100% !important; | |
| min-height: 100vh !important; | |
| max-height: 100vh !important; | |
| max-width: none !important; | |
| margin: 0 !important; | |
| overflow: hidden !important; | |
| } | |
| .gradio-container, .main.fillable, .contain { | |
| display: flex !important; | |
| flex-direction: column !important; | |
| padding: 0 !important; | |
| } | |
| .gradio-container { | |
| font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif !important; | |
| color: var(--weba-fg) !important; | |
| } | |
| footer, .footer, .built-with, .settings, .settings-bar, .show-api, | |
| .show-api-divider, .divider { | |
| display: none !important; | |
| } | |
| #app-shell { | |
| flex: 1 1 auto !important; | |
| height: 100% !important; | |
| max-height: 100% !important; | |
| min-height: 0 !important; | |
| display: flex !important; | |
| flex-direction: column !important; | |
| padding: 16px 24px 12px !important; | |
| box-sizing: border-box !important; | |
| overflow: hidden !important; | |
| background: var(--weba-canvas) !important; | |
| } | |
| #app-shell > .gap, #app-shell > div { | |
| background: transparent !important; | |
| flex-grow: 0 !important; | |
| height: auto !important; | |
| min-height: 0 !important; | |
| } | |
| #app-shell > #twil-chat, | |
| #app-shell > .block:has(#twil-chat), | |
| #app-shell > div:has(#twil-chat) { | |
| flex: 1 1 auto !important; | |
| min-height: 0 !important; | |
| height: auto !important; | |
| } | |
| .twil-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 4px 0 8px; | |
| flex-shrink: 0; | |
| } | |
| .twil-mark { width: 28px; height: 28px; border-radius: 6px; } | |
| .twil-wordmark { font-weight: 600; font-size: 15px; letter-spacing: -0.02em; line-height: 1.2; color: var(--weba-fg); } | |
| .twil-product { font-size: 12px; color: var(--weba-muted); line-height: 1.3; } | |
| .twil-cta { | |
| display: inline-flex; | |
| align-items: center; | |
| height: 34px; | |
| padding: 0 18px; | |
| border-radius: 999px; | |
| background: var(--weba-primary); | |
| color: var(--weba-primary-fg) !important; | |
| font-size: 13px; | |
| font-weight: 500; | |
| text-decoration: none !important; | |
| border: none; | |
| white-space: nowrap; | |
| transition: opacity 0.15s ease; | |
| } | |
| .twil-cta:hover { opacity: 0.85; } | |
| .twil-cta-header { | |
| /* Keep clear of the floating HF Space pill in the top-right corner */ | |
| margin-left: 20px; | |
| height: 30px; | |
| padding: 0 14px; | |
| font-size: 12px; | |
| } | |
| .twil-waitlist { | |
| margin-top: 28px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .twil-waitlist p { | |
| margin: 0; | |
| max-width: 30rem; | |
| font-size: 13px; | |
| line-height: 1.5; | |
| color: var(--weba-muted); | |
| } | |
| .twil-empty { | |
| height: 100%; | |
| min-height: 280px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| text-align: center; | |
| padding: 12px; | |
| } | |
| .twil-cube { width: 96px; height: 96px; margin-bottom: 24px; } | |
| .twil-empty h1 { | |
| font-size: 28px; | |
| font-weight: 500; | |
| letter-spacing: -0.03em; | |
| margin: 0 0 8px; | |
| color: var(--weba-fg); | |
| } | |
| .twil-empty p { | |
| margin: 0; | |
| max-width: 28rem; | |
| font-size: 15px; | |
| line-height: 1.5; | |
| color: var(--weba-muted); | |
| } | |
| #twil-chat, #twil-chat > .wrapper, #twil-chat .bubble-wrap, | |
| #twil-chat .message-wrap, #twil-chat .placeholder-content { | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| flex: 1 1 auto !important; | |
| min-height: 0 !important; | |
| } | |
| #twil-chat { | |
| max-width: 768px !important; | |
| width: 100% !important; | |
| margin: 0 auto !important; | |
| flex: 1 1 auto !important; | |
| } | |
| #twil-chat .message.user, #twil-chat .user, #twil-chat .bubble.user { | |
| background: var(--weba-secondary) !important; | |
| color: var(--weba-fg) !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| border-radius: 12px !important; | |
| } | |
| #twil-chat .message.bot, #twil-chat .bot, #twil-chat .bubble.bot { | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| } | |
| #twil-chat [aria-label="Delete"], #twil-chat [aria-label="Copy"], | |
| #twil-chat [aria-label="Share"], #twil-chat .message-buttons { | |
| display: none !important; | |
| } | |
| #twil-chat details, #twil-chat .thought, #twil-chat .md.thought { | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| color: var(--weba-muted) !important; | |
| } | |
| #composer-row { | |
| max-width: 768px !important; | |
| width: 100% !important; | |
| margin: 8px auto 0 !important; | |
| display: flex !important; | |
| flex-direction: row !important; | |
| flex-wrap: nowrap !important; | |
| align-items: center !important; | |
| gap: 10px !important; | |
| flex: 0 0 auto !important; | |
| min-height: 56px !important; | |
| background: transparent !important; | |
| } | |
| #composer-row > .block:has(#twil-input), | |
| #twil-input { | |
| flex: 1 1 0% !important; | |
| min-width: 0 !important; | |
| width: auto !important; | |
| height: 52px !important; | |
| min-height: 52px !important; | |
| max-height: 52px !important; | |
| background: var(--weba-composer) !important; | |
| border: 1px solid var(--weba-border) !important; | |
| border-radius: 999px !important; | |
| box-shadow: none !important; | |
| overflow: hidden !important; | |
| } | |
| #twil-input label, | |
| #twil-input .input-container { | |
| display: flex !important; | |
| align-items: stretch !important; | |
| width: 100% !important; | |
| height: 100% !important; | |
| min-height: 52px !important; | |
| margin: 0 !important; | |
| padding: 0 !important; | |
| border: none !important; | |
| background: transparent !important; | |
| box-sizing: border-box !important; | |
| } | |
| #twil-input textarea, | |
| #twil-input input, | |
| #twil-input .scroll-hide, | |
| #twil-input [data-testid="textbox"] { | |
| display: block !important; | |
| background: transparent !important; | |
| color: var(--weba-fg) !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| width: 100% !important; | |
| height: 52px !important; | |
| min-height: 52px !important; | |
| max-height: 52px !important; | |
| padding: 16px 18px !important; | |
| font-size: 16px !important; | |
| line-height: 20px !important; | |
| overflow-y: auto !important; | |
| overflow-x: hidden !important; | |
| resize: none !important; | |
| white-space: pre-wrap !important; | |
| field-sizing: fixed !important; | |
| pointer-events: auto !important; | |
| } | |
| #twil-input button, #twil-input .show-count, #twil-input .icon-button, | |
| #twil-input .sr-only, #twil-input span[data-testid="block-info"] { | |
| display: none !important; | |
| } | |
| #composer-row > .form, | |
| #composer-row > .block:has(#send-btn) { | |
| flex: 0 0 40px !important; | |
| width: 40px !important; | |
| min-width: 40px !important; | |
| max-width: 40px !important; | |
| background: transparent !important; | |
| border: none !important; | |
| padding: 0 !important; | |
| box-shadow: none !important; | |
| } | |
| #send-btn { | |
| flex: 0 0 40px !important; | |
| width: 40px !important; | |
| min-width: 40px !important; | |
| max-width: 40px !important; | |
| height: 40px !important; | |
| border-radius: 999px !important; | |
| background: var(--weba-secondary) !important; | |
| color: var(--weba-fg) !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| align-self: center !important; | |
| padding: 0 !important; | |
| } | |
| #chips-row { | |
| max-width: 768px !important; | |
| width: 100% !important; | |
| margin: 0 auto !important; | |
| display: flex !important; | |
| flex-wrap: wrap !important; | |
| justify-content: center !important; | |
| gap: 8px !important; | |
| flex-shrink: 0 !important; | |
| background: transparent !important; | |
| padding: 4px 0 8px !important; | |
| } | |
| #chips-row button { | |
| height: 32px !important; | |
| padding: 0 14px !important; | |
| border-radius: 999px !important; | |
| border: 1px solid var(--weba-border) !important; | |
| background: transparent !important; | |
| color: var(--weba-muted) !important; | |
| box-shadow: none !important; | |
| font-size: 13px !important; | |
| font-weight: 400 !important; | |
| } | |
| #chips-row button:hover { | |
| background: var(--weba-secondary) !important; | |
| color: var(--weba-fg) !important; | |
| } | |
| @keyframes twil-pulse { | |
| 0%, 100% { opacity: 0.4; } | |
| 50% { opacity: 1; } | |
| } | |
| #twil-chat .pending, #twil-chat [data-status="pending"] { | |
| animation: twil-pulse 1.1s ease-in-out infinite; | |
| } | |
| #params-box, | |
| #params-box.block, | |
| #params-box .wrap, | |
| #params-box .styler, | |
| #params-box .gap, | |
| #accordion-content { | |
| max-width: 768px !important; | |
| margin: 4px auto 0 !important; | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| flex: 0 0 auto !important; | |
| flex-grow: 0 !important; | |
| height: auto !important; | |
| min-height: 0 !important; | |
| max-height: none !important; | |
| overflow: visible !important; | |
| } | |
| #params-box .label-wrap, #params-box .icon { | |
| color: var(--weba-muted) !important; | |
| font-size: 12px !important; | |
| box-shadow: none !important; | |
| background: transparent !important; | |
| border: none !important; | |
| } | |
| .twil-disclaimer { | |
| margin: 8px 0 0; | |
| text-align: center; | |
| font-size: 11px; | |
| color: var(--weba-muted); | |
| flex-shrink: 0; | |
| } | |
| /* Tabs shell: keep the chat layout filling the viewport */ | |
| #twil-tabs { | |
| flex: 1 1 auto !important; | |
| min-height: 0 !important; | |
| display: flex !important; | |
| flex-direction: column !important; | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| } | |
| #twil-tabs > .tab-wrapper, | |
| #twil-tabs .tab-container, | |
| #twil-tabs .tab-nav { | |
| flex: 0 0 auto !important; | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| justify-content: center !important; | |
| } | |
| #twil-tabs .tab-container button, | |
| #twil-tabs .tab-nav button { | |
| background: transparent !important; | |
| border: none !important; | |
| border-bottom: 2px solid transparent !important; | |
| border-radius: 0 !important; | |
| box-shadow: none !important; | |
| color: var(--weba-muted) !important; | |
| font-size: 13px !important; | |
| padding: 6px 14px !important; | |
| } | |
| #twil-tabs .tab-container button.selected, | |
| #twil-tabs .tab-nav button.selected { | |
| color: var(--weba-fg) !important; | |
| border-bottom-color: var(--weba-fg) !important; | |
| } | |
| #twil-tabs .tabitem { | |
| flex: 1 1 auto !important; | |
| min-height: 0 !important; | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| padding: 0 !important; | |
| } | |
| #tab-chat > .gap, #tab-chat > .column, | |
| #tab-chat, #tab-chat .column:has(> div > #twil-chat) { | |
| height: 100% !important; | |
| min-height: 0 !important; | |
| } | |
| #tab-chat > .gap, #tab-chat > .column { | |
| display: flex !important; | |
| flex-direction: column !important; | |
| } | |
| #tab-chat .block:has(#twil-chat), | |
| #tab-chat div:has(> #twil-chat) { | |
| flex: 1 1 auto !important; | |
| min-height: 0 !important; | |
| height: auto !important; | |
| } | |
| #tab-showcase { | |
| overflow-y: auto !important; | |
| } | |
| /* Showcase cards */ | |
| .twil-showcase-intro { | |
| max-width: 768px; | |
| margin: 8px auto 4px; | |
| text-align: center; | |
| } | |
| .twil-showcase-intro h2 { | |
| font-size: 22px; | |
| font-weight: 500; | |
| letter-spacing: -0.02em; | |
| margin: 0 0 6px; | |
| color: var(--weba-fg); | |
| } | |
| .twil-showcase-intro p { | |
| margin: 0 0 8px; | |
| font-size: 14px; | |
| line-height: 1.5; | |
| color: var(--weba-muted); | |
| } | |
| .twil-card-row { | |
| max-width: 900px !important; | |
| width: 100% !important; | |
| margin: 6px auto !important; | |
| display: flex !important; | |
| gap: 12px !important; | |
| align-items: stretch !important; | |
| background: transparent !important; | |
| } | |
| .twil-card { | |
| background: var(--weba-composer) !important; | |
| border: 1px solid var(--weba-border) !important; | |
| border-radius: 16px !important; | |
| box-shadow: none !important; | |
| padding: 16px !important; | |
| gap: 10px !important; | |
| display: flex !important; | |
| flex-direction: column !important; | |
| justify-content: space-between !important; | |
| min-height: 170px !important; | |
| } | |
| .twil-card > * { background: transparent !important; } | |
| .twil-card-kicker { | |
| font-size: 11px; | |
| font-weight: 600; | |
| letter-spacing: 0.06em; | |
| text-transform: uppercase; | |
| color: var(--weba-muted); | |
| margin-bottom: 8px; | |
| } | |
| .twil-card-quote { | |
| font-size: 14px; | |
| line-height: 1.45; | |
| color: var(--weba-fg); | |
| margin-bottom: 8px; | |
| } | |
| .twil-card-hint { | |
| font-size: 12px; | |
| color: var(--weba-muted); | |
| font-style: italic; | |
| } | |
| .twil-card-btn { | |
| height: 32px !important; | |
| border-radius: 999px !important; | |
| border: 1px solid var(--weba-border) !important; | |
| background: var(--weba-secondary) !important; | |
| color: var(--weba-fg) !important; | |
| box-shadow: none !important; | |
| font-size: 13px !important; | |
| font-weight: 500 !important; | |
| align-self: flex-start !important; | |
| padding: 0 16px !important; | |
| width: auto !important; | |
| min-width: 0 !important; | |
| flex: 0 0 auto !important; | |
| } | |
| .twil-card-btn:hover { | |
| background: var(--weba-fg) !important; | |
| color: var(--weba-canvas) !important; | |
| } | |
| """ | |
| THEME = gr.themes.Base( | |
| primary_hue="zinc", | |
| secondary_hue="zinc", | |
| neutral_hue="zinc", | |
| font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"], | |
| radius_size=gr.themes.sizes.radius_lg, | |
| ).set( | |
| body_background_fill="#f4f4f4", | |
| body_background_fill_dark="#232323", | |
| body_text_color="#161616", | |
| body_text_color_dark="#fafafa", | |
| background_fill_primary="transparent", | |
| background_fill_primary_dark="transparent", | |
| background_fill_secondary="#e8e8e8", | |
| background_fill_secondary_dark="#323232", | |
| border_color_primary="rgba(22,22,22,0.1)", | |
| border_color_primary_dark="rgba(255,255,255,0.1)", | |
| block_background_fill="transparent", | |
| block_background_fill_dark="transparent", | |
| block_border_width="0px", | |
| block_shadow="none", | |
| block_shadow_dark="none", | |
| shadow_drop="none", | |
| shadow_drop_lg="none", | |
| input_background_fill="#ffffff", | |
| input_background_fill_dark="#161616", | |
| input_border_color="rgba(22,22,22,0.1)", | |
| input_border_color_dark="rgba(255,255,255,0.1)", | |
| input_shadow="none", | |
| button_secondary_background_fill="#e8e8e8", | |
| button_secondary_background_fill_dark="#323232", | |
| button_secondary_text_color="#161616", | |
| button_secondary_text_color_dark="#fafafa", | |
| button_secondary_shadow="none", | |
| ) | |
| tokenizer = None | |
| model = None | |
| EOS_TOKEN_ID = None | |
| if not PREVIEW: | |
| tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) | |
| model = AutoModelForCausalLM.from_pretrained( | |
| MODEL_ID, | |
| dtype=torch.bfloat16, | |
| attn_implementation="sdpa", | |
| ).to("cuda") | |
| model.eval() | |
| model.generation_config.use_cache = True | |
| EOS_TOKEN_ID = tokenizer.eos_token_id | |
| def _strip_thinking(text: str) -> str: | |
| if not isinstance(text, str): | |
| return "" | |
| return _THINK_RE.sub("", text).strip() | |
| def _parse_think(raw: str) -> tuple[str, str, bool]: | |
| """Return (think, answer, think_open) from a possibly streaming reply.""" | |
| open_idx = raw.find(THINK_OPEN) | |
| if open_idx == -1: | |
| # A partially streamed opening tag is not an answer yet. | |
| if raw.strip() and THINK_OPEN.startswith(raw.strip()): | |
| return "", "", False | |
| return "", raw, False | |
| rest = raw[open_idx + len(THINK_OPEN) :] | |
| close_idx = rest.find(THINK_CLOSE) | |
| before = raw[:open_idx] | |
| if close_idx == -1: | |
| # Hide a partially streamed closing tag at the end of the think block. | |
| for i in range(len(THINK_CLOSE) - 1, 0, -1): | |
| if rest.endswith(THINK_CLOSE[:i]): | |
| rest = rest[:-i] | |
| break | |
| return rest.strip(), before.strip(), True | |
| think = rest[:close_idx].strip() | |
| answer = (before + rest[close_idx + len(THINK_CLOSE) :]).strip() | |
| return think, answer, False | |
| def _gpu_seconds(history, max_new_tokens=1024, *args, **kwargs): | |
| # ZeroGPU adds its own startup overhead on top of this request, and | |
| # anonymous visitors have a small quota — keep the ask modest. | |
| tokens = int(max_new_tokens or 1024) | |
| return min(90, max(25, 10 + tokens // 30)) | |
| def _history_for_model(history: list) -> list[dict]: | |
| conversation = [{"role": "system", "content": SYSTEM_PROMPT}] | |
| for msg in history or []: | |
| role = msg.get("role", "user") | |
| meta = msg.get("metadata") or {} | |
| # Only skip thought/tool bubbles; Gradio attaches an empty metadata | |
| # dict to ordinary messages, so a bare truthiness check drops them all. | |
| is_thought = isinstance(meta, dict) and bool(meta.get("title")) | |
| if role == "system" or is_thought: | |
| continue | |
| content = msg.get("content", "") | |
| if isinstance(content, list): | |
| # Gradio 6 sends message content as a list of blocks. | |
| parts = [] | |
| for block in content: | |
| if isinstance(block, str): | |
| parts.append(block) | |
| elif isinstance(block, dict): | |
| text = block.get("text") or block.get("content") or "" | |
| if isinstance(text, str) and text: | |
| parts.append(text) | |
| content = "\n".join(parts) | |
| if role == "assistant": | |
| content = _strip_thinking(content) | |
| if content: | |
| conversation.append({"role": role, "content": content}) | |
| return conversation | |
| def _stream_tokens(conversation: list[dict], max_new_tokens: int, temperature: float, top_p: float, enable_thinking: bool) -> Iterator[str]: | |
| if PREVIEW: | |
| import time | |
| demo = ( | |
| f"{THINK_OPEN}\n" | |
| "Check the premises, then the conclusion. Premise 1 gives Rain -> Wet. " | |
| "Premise 2 observes Wet. Inferring Rain from Wet affirms the consequent, " | |
| "which is invalid. The argument is a classic Barbara syllogism otherwise.\n" | |
| f"{THINK_CLOSE}\n\n" | |
| "Answer: entailment." | |
| ) | |
| acc = "" | |
| for ch in demo: | |
| acc += ch | |
| time.sleep(0.02) | |
| yield acc | |
| return | |
| encoded = tokenizer.apply_chat_template( | |
| conversation, | |
| add_generation_prompt=True, | |
| return_tensors="pt", | |
| return_dict=True, | |
| enable_thinking=enable_thinking, | |
| ) | |
| input_ids = encoded["input_ids"] | |
| attention_mask = encoded["attention_mask"] | |
| if input_ids.shape[1] > MAX_INPUT_TOKEN_LENGTH: | |
| input_ids = input_ids[:, -MAX_INPUT_TOKEN_LENGTH:] | |
| attention_mask = attention_mask[:, -MAX_INPUT_TOKEN_LENGTH:] | |
| gr.Warning(f"Trimmed the conversation to the last {MAX_INPUT_TOKEN_LENGTH} tokens.") | |
| input_ids = input_ids.to(model.device) | |
| attention_mask = attention_mask.to(model.device) | |
| streamer = TextIteratorStreamer( | |
| tokenizer, timeout=30.0, skip_prompt=True, skip_special_tokens=True | |
| ) | |
| generate_kwargs = dict( | |
| input_ids=input_ids, | |
| attention_mask=attention_mask, | |
| streamer=streamer, | |
| max_new_tokens=int(max_new_tokens), | |
| num_beams=1, | |
| use_cache=True, | |
| eos_token_id=EOS_TOKEN_ID, | |
| pad_token_id=tokenizer.pad_token_id or EOS_TOKEN_ID, | |
| ) | |
| if temperature == 0: | |
| generate_kwargs["do_sample"] = False | |
| else: | |
| generate_kwargs["do_sample"] = True | |
| generate_kwargs["temperature"] = float(temperature) | |
| generate_kwargs["top_p"] = float(top_p) | |
| Thread(target=model.generate, kwargs=generate_kwargs, daemon=True).start() | |
| acc = "" | |
| for text in streamer: | |
| acc += text | |
| yield acc | |
| def queue_message(message, history): | |
| """Paint the user turn immediately so the GPU wait is not a blank screen.""" | |
| message = (message or "").strip() | |
| if not message: | |
| return "", history, gr.skip() | |
| history = [dict(m) if isinstance(m, dict) else m for m in (history or [])] | |
| history.append({"role": "user", "content": message}) | |
| history.append( | |
| { | |
| "role": "assistant", | |
| "content": "", | |
| "metadata": {"title": "Logicizing…", "status": "pending"}, | |
| } | |
| ) | |
| return "", history, gr.update(visible=False) | |
| def generate_reply(history, max_new_tokens, temperature, top_p, enable_thinking): | |
| history = [dict(m) if isinstance(m, dict) else m for m in (history or [])] | |
| if not any(m.get("role") == "user" for m in history): | |
| yield history | |
| return | |
| conversation = _history_for_model(history) | |
| # Stream only into a bubble at the END of the history. Reaching for any | |
| # older assistant message would overwrite a previous turn in place. | |
| think_msg = None | |
| last = history[-1] if history else None | |
| if ( | |
| last is not None | |
| and last.get("role") == "assistant" | |
| and isinstance(last.get("metadata"), dict) | |
| and last["metadata"].get("status") == "pending" | |
| ): | |
| think_msg = last | |
| think_msg["content"] = "" | |
| elif last is not None and last.get("role") == "user": | |
| # Gradio strips the empty pending bubble in preprocessing; recreate it. | |
| think_msg = {"role": "assistant", "content": ""} | |
| history.append(think_msg) | |
| else: | |
| # No new user turn (e.g. an empty submit); don't regenerate old answers. | |
| yield history | |
| return | |
| think_msg["metadata"] = {"title": "Logicizing…", "status": "pending"} | |
| yield history | |
| answer_msg = {"role": "assistant", "content": ""} | |
| for raw in _stream_tokens( | |
| conversation, max_new_tokens, temperature, top_p, enable_thinking | |
| ): | |
| think, answer, think_open = _parse_think(raw) | |
| if think or think_open: | |
| think_msg["content"] = think | |
| think_msg["metadata"] = { | |
| "title": "Logicizing…" if think_open else "Logic", | |
| "status": "pending" if think_open else "done", | |
| } | |
| if answer: | |
| answer_msg["content"] = answer | |
| if not any(m is answer_msg for m in history): | |
| history.append(answer_msg) | |
| elif not think_open and not think and raw.strip(): | |
| answer_msg["content"] = raw.strip() | |
| if not any(m is answer_msg for m in history): | |
| history.append(answer_msg) | |
| elif any(m is answer_msg for m in history): | |
| # A partial tag was mistaken for an answer earlier; retract it. | |
| history.remove(answer_msg) | |
| answer_msg["content"] = "" | |
| yield history | |
| if think_msg["content"]: | |
| think_msg["metadata"] = {"title": "Logic", "status": "done"} | |
| yield history | |
| elif think_msg in history and not think_msg["content"]: | |
| history.remove(think_msg) | |
| yield history | |
| with gr.Blocks(fill_height=True, fill_width=True, elem_id="app-root") as demo: | |
| with gr.Column(elem_id="app-shell"): | |
| gr.HTML(HEADER) | |
| with gr.Tabs(elem_id="twil-tabs") as tabs: | |
| with gr.Tab("Chat", id="chat", elem_id="tab-chat"): | |
| chatbot = gr.Chatbot( | |
| value=[], | |
| placeholder=PLACEHOLDER, | |
| label="", | |
| show_label=False, | |
| layout="bubble", | |
| avatar_images=(None, str(AVATAR) if AVATAR.exists() else None), | |
| allow_tags=["think"], | |
| reasoning_tags=[(THINK_OPEN, THINK_CLOSE)], | |
| buttons=None, | |
| editable=False, | |
| line_breaks=False, | |
| elem_id="twil-chat", | |
| scale=1, | |
| height="100%", | |
| ) | |
| with gr.Row(elem_id="chips-row") as chips_row: | |
| chip_btns = [ | |
| gr.Button(label, variant="secondary", size="sm", elem_classes=["twil-chip"]) | |
| for label, _ in CHIPS | |
| ] | |
| with gr.Row(elem_id="composer-row"): | |
| prompt = gr.Textbox( | |
| placeholder="Ask webAI anything…", | |
| show_label=False, | |
| container=False, | |
| lines=2, | |
| max_lines=2, | |
| elem_id="twil-input", | |
| scale=8, | |
| ) | |
| send = gr.Button("↑", elem_id="send-btn", scale=0) | |
| with gr.Accordion("Parameters", open=False, elem_id="params-box"): | |
| max_new_tokens = gr.Slider(256, 4096, value=1024, step=256, label="Max new tokens") | |
| temperature = gr.Slider(0, 1.5, value=0, step=0.05, label="Temperature (0 = greedy)") | |
| top_p = gr.Slider(0.1, 1.0, value=0.95, step=0.05, label="Top-p") | |
| enable_thinking = gr.Checkbox(value=True, label="Enable thinking") | |
| gr.HTML('<p class="twil-disclaimer">This is AI and it can make mistakes</p>') | |
| with gr.Tab("Showcase", id="showcase", elem_id="tab-showcase"): | |
| gr.HTML(SHOWCASE_INTRO) | |
| card_btns = [] | |
| for start in range(0, len(SHOWCASE), 3): | |
| with gr.Row(elem_classes=["twil-card-row"]): | |
| for case in SHOWCASE[start : start + 3]: | |
| with gr.Column(elem_classes=["twil-card"], scale=1, min_width=220): | |
| gr.HTML(_card_html(case)) | |
| card_btns.append( | |
| gr.Button("Analyze it", size="sm", elem_classes=["twil-card-btn"]) | |
| ) | |
| # Lock the composer while a turn is running: overlapping events make the | |
| # finishing stream overwrite the chat with its own stale history. | |
| lockable = [prompt, send, *card_btns] | |
| def _lock(): | |
| return [gr.update(interactive=False)] * len(lockable) | |
| def _unlock(): | |
| return [gr.update(interactive=True)] * len(lockable) | |
| def _wire(event, prep_fn=None, prep_outputs=None): | |
| chain = event(_lock, None, lockable, show_progress="hidden") | |
| if prep_fn is not None: | |
| chain = chain.then(prep_fn, outputs=prep_outputs, show_progress="hidden") | |
| chain = chain.then( | |
| queue_message, | |
| [prompt, chatbot], | |
| [prompt, chatbot, chips_row], | |
| show_progress="hidden", | |
| ).then( | |
| generate_reply, | |
| [chatbot, max_new_tokens, temperature, top_p, enable_thinking], | |
| chatbot, | |
| concurrency_limit=1, | |
| show_progress="hidden", | |
| ) | |
| chain.then(_unlock, None, lockable, show_progress="hidden") | |
| _wire(send.click) | |
| _wire(prompt.submit) | |
| for btn, (_, template) in zip(chip_btns, CHIPS): | |
| btn.click(lambda t=template: t, outputs=prompt) | |
| for btn, case in zip(card_btns, SHOWCASE): | |
| case_prompt = SHOWCASE_TEMPLATE.format(argument=case["quote"]) | |
| _wire( | |
| btn.click, | |
| prep_fn=lambda p=case_prompt: (p, gr.Tabs(selected="chat")), | |
| prep_outputs=[prompt, tabs], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch( | |
| theme=THEME, | |
| css=CSS, | |
| head=HEAD, | |
| allowed_paths=[str(ASSETS), str(ASSETS.parent)], | |
| ssr_mode=False, | |
| show_error=True, | |
| ) | |