David-stout commited on
Commit
9d703b3
·
verified ·
1 Parent(s): 71d5a71

Polish webAI UI and collapse Logic traces by default

Browse files
Files changed (1) hide show
  1. app.py +345 -225
app.py CHANGED
@@ -4,22 +4,29 @@ from collections.abc import Iterator
4
  from pathlib import Path
5
  from threading import Thread
6
 
7
- import spaces
8
- import torch
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  import gradio as gr
10
- from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
11
 
12
  MODEL_ID = "webAI-Official/TwIL-LM3"
13
  MAX_INPUT_TOKEN_LENGTH = int(os.getenv("MAX_INPUT_TOKEN_LENGTH", "8192"))
14
  ASSETS = Path(__file__).resolve().parent / "brand"
15
- ICON = ASSETS / "AppiCon.svg"
16
- CUBE = ASSETS / "webai-cube-256.webp"
17
  AVATAR = ASSETS / "avatar.svg"
18
 
19
- # Reasoning delimiters are non-special tokens (ids 128002 / 128003), so they
20
- # survive skip_special_tokens=True and Gradio can render them as a collapsible
21
- # section via allow_tags=["think"]. Built with chr() so the literal tags are
22
- # not written into the source.
23
  THINK_OPEN = chr(60) + "think" + chr(62)
24
  THINK_CLOSE = chr(60) + chr(47) + "think" + chr(62)
25
  _THINK_RE = re.compile(
@@ -33,6 +40,21 @@ SYSTEM_PROMPT = (
33
  f"Work through the problem in a {THINK_OPEN} block, then give a concise, precise answer."
34
  )
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  HEADER = """
37
  <div class="twil-header">
38
  <img class="twil-mark" src="/gradio_api/file=brand/AppiCon.svg" alt="webAI" />
@@ -51,28 +73,18 @@ PLACEHOLDER = """
51
  </div>
52
  """
53
 
54
- FOOTER = """
55
- <p class="twil-disclaimer">This is AI and it can make mistakes</p>
56
- """
57
-
58
  CSS = """
59
  :root {
60
- --weba-bg: #ffffff;
61
- --weba-subtle: #f4f4f4;
62
  --weba-canvas: #f4f4f4;
63
  --weba-fg: #161616;
64
  --weba-muted: #737373;
65
  --weba-secondary: #e8e8e8;
66
- --weba-border: #e8e8e8;
67
  --weba-composer: #ffffff;
68
  --weba-primary: #232323;
69
  --weba-primary-fg: #fafafa;
70
- --weba-link: #2563eb;
71
- --weba-radius: 12px;
72
  }
73
  .dark {
74
- --weba-bg: #161616;
75
- --weba-subtle: #232323;
76
  --weba-canvas: #232323;
77
  --weba-fg: #fafafa;
78
  --weba-muted: #a8a8a8;
@@ -81,143 +93,215 @@ CSS = """
81
  --weba-composer: #161616;
82
  --weba-primary: #e8e8e8;
83
  --weba-primary-fg: #232323;
84
- --weba-link: #3b82f6;
85
  }
86
- html, body, .gradio-container, .gradio-container .main,
87
- .gradio-container .contain, .fillable, .gradio-container .column {
 
 
 
88
  background: var(--weba-canvas) !important;
89
- color: var(--weba-fg) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  }
91
  .gradio-container {
92
- max-width: 768px !important;
93
- margin: 0 auto !important;
94
- padding: 12px 16px 24px !important;
95
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  }
97
- .dark .gradio-container { color: var(--weba-fg); }
98
- footer, .footer, .built-with, .settings-bar { display: none !important; }
99
- a { color: var(--weba-link) !important; }
100
 
101
  .twil-header {
102
  display: flex;
103
  align-items: center;
104
  gap: 12px;
105
- padding: 8px 4px 18px;
106
- }
107
- .twil-mark {
108
- width: 28px;
109
- height: 28px;
110
- border-radius: 6px;
111
- }
112
- .twil-wordmark {
113
- font-weight: 600;
114
- font-size: 15px;
115
- letter-spacing: -0.02em;
116
- line-height: 1.2;
117
- }
118
- .twil-product {
119
- font-size: 12px;
120
- color: var(--weba-muted);
121
- line-height: 1.3;
122
  }
 
 
 
 
123
  .twil-empty {
124
- padding: 48px 24px 32px;
125
- text-align: center;
126
  display: flex;
127
  flex-direction: column;
128
  align-items: center;
 
 
 
129
  }
130
- .twil-cube {
131
- width: 96px;
132
- height: 96px;
133
- margin-bottom: 28px;
134
- }
135
  .twil-empty h1 {
136
  font-size: 28px;
137
  font-weight: 500;
138
  letter-spacing: -0.03em;
139
- margin: 0 0 10px;
140
  color: var(--weba-fg);
141
  }
142
  .twil-empty p {
143
  margin: 0;
144
  max-width: 28rem;
145
- font-size: 16px;
146
  line-height: 1.5;
147
  color: var(--weba-muted);
148
  }
149
- .twil-disclaimer {
150
- margin: 10px 0 0;
151
- text-align: center;
152
- font-size: 11px;
153
- color: var(--weba-muted);
154
- opacity: 0.85;
155
- }
156
 
157
- #twil-chat {
 
158
  background: transparent !important;
159
  border: none !important;
160
  box-shadow: none !important;
 
 
161
  }
162
- #twil-chat .wrapper, #twil-chat .bubble-wrap, #twil-chat .message-wrap {
163
- background: transparent !important;
 
 
164
  }
165
- #twil-chat .user, #twil-chat .message.user, #twil-chat .user-row .message,
166
- #twil-chat .bubble.user, #twil-chat [data-testid="user"] {
167
  background: var(--weba-secondary) !important;
168
  color: var(--weba-fg) !important;
169
  border: none !important;
 
170
  border-radius: 12px !important;
171
  }
172
- #twil-chat .bot, #twil-chat .message.bot, #twil-chat .bot-row .message,
173
- #twil-chat .bubble.bot {
174
  background: transparent !important;
175
  border: none !important;
176
  box-shadow: none !important;
177
  }
 
 
 
 
 
 
 
 
 
 
178
 
179
- #twil-input textarea, #twil-input input, .input-container textarea {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  background: var(--weba-composer) !important;
181
  color: var(--weba-fg) !important;
182
  border: 1px solid var(--weba-border) !important;
183
  border-radius: 999px !important;
184
- padding: 14px 18px !important;
185
  font-size: 16px !important;
186
  box-shadow: none !important;
 
187
  }
188
- .input-container, #twil-input {
 
189
  background: transparent !important;
190
  border: none !important;
191
  box-shadow: none !important;
 
192
  }
193
 
194
- button.primary, .primary {
195
- background: var(--weba-primary) !important;
196
- color: var(--weba-primary-fg) !important;
197
- border: none !important;
198
  border-radius: 999px !important;
199
- }
200
- button.secondary, .secondary {
201
  background: var(--weba-secondary) !important;
202
  color: var(--weba-fg) !important;
203
- border: 1px solid var(--weba-border) !important;
204
- border-radius: 999px !important;
 
205
  }
206
 
207
- .examples, .samples {
208
- gap: 8px !important;
209
- }
210
- .examples button, .sample {
211
- background: var(--weba-bg) !important;
212
- color: var(--weba-fg) !important;
213
- border: 1px solid var(--weba-border) !important;
214
- border-radius: 999px !important;
215
- font-size: 13px !important;
216
- }
217
- .accordion, .label-wrap {
218
  background: transparent !important;
219
- border-color: var(--weba-border) !important;
 
 
 
 
220
  color: var(--weba-muted) !important;
 
 
 
 
 
 
 
 
 
 
 
 
221
  }
222
  """
223
 
@@ -226,92 +310,108 @@ THEME = gr.themes.Base(
226
  secondary_hue="zinc",
227
  neutral_hue="zinc",
228
  font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
229
- font_mono=["ui-monospace", "SFMono-Regular", "Menlo", "monospace"],
230
  radius_size=gr.themes.sizes.radius_lg,
231
  ).set(
232
  body_background_fill="#f4f4f4",
233
  body_background_fill_dark="#232323",
234
  body_text_color="#161616",
235
  body_text_color_dark="#fafafa",
236
- background_fill_primary="#ffffff",
237
- background_fill_primary_dark="#161616",
238
  background_fill_secondary="#e8e8e8",
239
  background_fill_secondary_dark="#323232",
240
- border_color_primary="#e8e8e8",
241
  border_color_primary_dark="rgba(255,255,255,0.1)",
242
  block_background_fill="transparent",
243
  block_background_fill_dark="transparent",
244
  block_border_width="0px",
245
  block_shadow="none",
246
  block_shadow_dark="none",
247
- button_primary_background_fill="#232323",
248
- button_primary_background_fill_dark="#e8e8e8",
249
- button_primary_text_color="#fafafa",
250
- button_primary_text_color_dark="#232323",
 
 
 
251
  button_secondary_background_fill="#e8e8e8",
252
  button_secondary_background_fill_dark="#323232",
253
  button_secondary_text_color="#161616",
254
  button_secondary_text_color_dark="#fafafa",
255
- input_background_fill="#ffffff",
256
- input_background_fill_dark="#161616",
257
- input_border_color="#e8e8e8",
258
- input_border_color_dark="rgba(255,255,255,0.1)",
259
  )
260
 
261
- tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
262
- model = AutoModelForCausalLM.from_pretrained(
263
- MODEL_ID,
264
- dtype=torch.bfloat16,
265
- attn_implementation="sdpa",
266
- ).to("cuda")
267
- model.eval()
268
- model.generation_config.use_cache = True
269
 
270
- EOS_TOKEN_ID = tokenizer.eos_token_id
 
 
 
 
 
 
 
 
 
271
 
272
 
273
  def _strip_thinking(text: str) -> str:
274
- """Drop the reasoning trace from a prior assistant turn before re-prompting."""
275
  if not isinstance(text, str):
276
  return ""
277
  return _THINK_RE.sub("", text).strip()
278
 
279
 
280
- def _gpu_seconds(
281
- message,
282
- history,
283
- max_new_tokens=2048,
284
- temperature=0,
285
- top_p=0.95,
286
- enable_thinking=True,
287
- *args,
288
- **kwargs,
289
- ):
 
 
 
 
 
 
290
  tokens = int(max_new_tokens or 2048)
291
  return min(180, max(45, 25 + tokens // 18))
292
 
293
 
294
- @spaces.GPU(duration=_gpu_seconds)
295
- def chat_twil_lm3(
296
- message: str,
297
- history: list,
298
- max_new_tokens: int,
299
- temperature: float,
300
- top_p: float,
301
- enable_thinking: bool,
302
- ) -> Iterator[str]:
303
  conversation = [{"role": "system", "content": SYSTEM_PROMPT}]
304
  for msg in history or []:
305
  role = msg.get("role", "user")
306
- content = msg.get("content", "")
307
- if role == "system":
308
  continue
 
309
  if isinstance(content, list):
310
  content = ""
311
  if role == "assistant":
312
  content = _strip_thinking(content)
313
- conversation.append({"role": role, "content": content})
314
- conversation.append({"role": "user", "content": message})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
 
316
  encoded = tokenizer.apply_chat_template(
317
  conversation,
@@ -325,13 +425,10 @@ def chat_twil_lm3(
325
  if input_ids.shape[1] > MAX_INPUT_TOKEN_LENGTH:
326
  input_ids = input_ids[:, -MAX_INPUT_TOKEN_LENGTH:]
327
  attention_mask = attention_mask[:, -MAX_INPUT_TOKEN_LENGTH:]
328
- gr.Warning(
329
- f"Trimmed the conversation to the last {MAX_INPUT_TOKEN_LENGTH} tokens."
330
- )
331
 
332
  input_ids = input_ids.to(model.device)
333
  attention_mask = attention_mask.to(model.device)
334
-
335
  streamer = TextIteratorStreamer(
336
  tokenizer, timeout=30.0, skip_prompt=True, skip_special_tokens=True
337
  )
@@ -353,100 +450,123 @@ def chat_twil_lm3(
353
  generate_kwargs["top_p"] = float(top_p)
354
 
355
  Thread(target=model.generate, kwargs=generate_kwargs, daemon=True).start()
356
-
357
- chunks: list[str] = []
358
  for text in streamer:
359
- chunks.append(text)
360
- yield "".join(chunks)
361
-
362
-
363
- chatbot = gr.Chatbot(
364
- height=520,
365
- placeholder=PLACEHOLDER,
366
- label="",
367
- show_label=False,
368
- allow_tags=["think"],
369
- line_breaks=False,
370
- layout="bubble",
371
- avatar_images=(None, str(AVATAR) if AVATAR.exists() else None),
372
- elem_id="twil-chat",
373
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
 
375
- with gr.Blocks(fill_height=True) as demo:
376
- with gr.Column(elem_id="col-container"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  gr.HTML(HEADER)
378
- gr.ChatInterface(
379
- fn=chat_twil_lm3,
380
- chatbot=chatbot,
381
- textbox=gr.Textbox(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  placeholder="Ask webAI anything…",
383
  show_label=False,
384
  container=False,
 
 
385
  elem_id="twil-input",
386
- ),
387
- fill_height=True,
388
- concurrency_limit=1,
389
- additional_inputs_accordion=gr.Accordion(
390
- label="Parameters", open=False, render=False
391
- ),
392
- additional_inputs=[
393
- gr.Slider(
394
- minimum=256,
395
- maximum=4096,
396
- step=256,
397
- value=2048,
398
- label="Max new tokens",
399
- render=False,
400
- info="Keep this at 2048+ so the reasoning trace is not truncated.",
401
- ),
402
- gr.Slider(
403
- minimum=0,
404
- maximum=1.5,
405
- step=0.05,
406
- value=0,
407
- label="Temperature (0 = greedy)",
408
- render=False,
409
- info="Greedy (0) matches the published evaluation.",
410
- ),
411
- gr.Slider(
412
- minimum=0.1,
413
- maximum=1.0,
414
- step=0.05,
415
- value=0.95,
416
- label="Top-p",
417
- render=False,
418
- info="Used only when temperature > 0.",
419
- ),
420
- gr.Checkbox(
421
- value=True,
422
- label="Enable thinking (reasoning trace)",
423
- render=False,
424
- info="When on, the model reasons in a hidden block before answering.",
425
- ),
426
- ],
427
- examples=[
428
- [
429
- "Does 'All dogs are mammals. Rex is a dog.' entail 'Rex is a mammal'? "
430
- "Answer entailment, contradiction, or neutral."
431
- ],
432
- [
433
- "Translate into first-order logic: Every student who studies hard passes at least one exam."
434
- ],
435
- ["Formalize in Lean 4: If n is even, then n^2 is even."],
436
- [
437
- "Is 'All birds fly. Tweety is a bird. Therefore Tweety flies.' logically valid? Explain."
438
- ],
439
- [
440
- "Formalize and evaluate: If it rains, the ground is wet. The ground is not wet. Therefore it did not rain."
441
- ],
442
- ],
443
- cache_examples=False,
444
- )
445
- gr.HTML(FOOTER)
446
 
447
  if __name__ == "__main__":
448
  demo.launch(
449
  theme=THEME,
450
  css=CSS,
451
  allowed_paths=[str(ASSETS), str(ASSETS.parent)],
 
452
  )
 
4
  from pathlib import Path
5
  from threading import Thread
6
 
7
+ PREVIEW = os.getenv("TWIL_UI_PREVIEW") == "1"
8
+
9
+ if not PREVIEW:
10
+ import spaces
11
+ import torch
12
+ from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
13
+ else:
14
+
15
+ class spaces: # type: ignore[no-redef]
16
+ @staticmethod
17
+ def GPU(duration=None, **kwargs):
18
+ def decorator(fn):
19
+ return fn
20
+
21
+ return decorator
22
+
23
  import gradio as gr
 
24
 
25
  MODEL_ID = "webAI-Official/TwIL-LM3"
26
  MAX_INPUT_TOKEN_LENGTH = int(os.getenv("MAX_INPUT_TOKEN_LENGTH", "8192"))
27
  ASSETS = Path(__file__).resolve().parent / "brand"
 
 
28
  AVATAR = ASSETS / "avatar.svg"
29
 
 
 
 
 
30
  THINK_OPEN = chr(60) + "think" + chr(62)
31
  THINK_CLOSE = chr(60) + chr(47) + "think" + chr(62)
32
  _THINK_RE = re.compile(
 
40
  f"Work through the problem in a {THINK_OPEN} block, then give a concise, precise answer."
41
  )
42
 
43
+ CHIPS = [
44
+ (
45
+ "Analyze an argument",
46
+ "Analyze this argument: identify the premises and the conclusion, say whether it is valid, and name any fallacy.\n\n",
47
+ ),
48
+ (
49
+ "Check an entailment",
50
+ "Do these premises entail the conclusion? Give a formal proof or a counterexample.\n\nPremise 1: \nPremise 2: \nConclusion: ",
51
+ ),
52
+ (
53
+ "Translate to FOL",
54
+ "Translate into first-order logic: Every student who studies hard passes at least one exam.",
55
+ ),
56
+ ]
57
+
58
  HEADER = """
59
  <div class="twil-header">
60
  <img class="twil-mark" src="/gradio_api/file=brand/AppiCon.svg" alt="webAI" />
 
73
  </div>
74
  """
75
 
 
 
 
 
76
  CSS = """
77
  :root {
 
 
78
  --weba-canvas: #f4f4f4;
79
  --weba-fg: #161616;
80
  --weba-muted: #737373;
81
  --weba-secondary: #e8e8e8;
82
+ --weba-border: rgba(22, 22, 22, 0.1);
83
  --weba-composer: #ffffff;
84
  --weba-primary: #232323;
85
  --weba-primary-fg: #fafafa;
 
 
86
  }
87
  .dark {
 
 
88
  --weba-canvas: #232323;
89
  --weba-fg: #fafafa;
90
  --weba-muted: #a8a8a8;
 
93
  --weba-composer: #161616;
94
  --weba-primary: #e8e8e8;
95
  --weba-primary-fg: #232323;
 
96
  }
97
+
98
+ * { box-shadow: none !important; text-shadow: none !important; }
99
+ html, body, #root, .gradio-container, .gradio-container > .main,
100
+ .gradio-container .contain, .fillable, .gradio-container .wrap,
101
+ .gradio-container .column, .gradio-container .row, .contain, .wrapper {
102
  background: var(--weba-canvas) !important;
103
+ box-shadow: none !important;
104
+ filter: none !important;
105
+ }
106
+ html, body, .gradio-container, .gradio-container > .main, .fillable,
107
+ .contain, .gradio-container .wrap, .app {
108
+ height: 100% !important;
109
+ min-height: 100vh !important;
110
+ max-height: 100vh !important;
111
+ max-width: none !important;
112
+ margin: 0 !important;
113
+ overflow: hidden !important;
114
+ }
115
+ .gradio-container, .main.fillable, .contain {
116
+ display: flex !important;
117
+ flex-direction: column !important;
118
+ padding: 0 !important;
119
  }
120
  .gradio-container {
 
 
 
121
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif !important;
122
+ color: var(--weba-fg) !important;
123
+ }
124
+ footer, .footer, .built-with, .settings, .settings-bar, .show-api,
125
+ .show-api-divider, .divider {
126
+ display: none !important;
127
+ }
128
+
129
+ #app-shell {
130
+ flex: 1 1 auto !important;
131
+ height: 100% !important;
132
+ max-height: 100% !important;
133
+ min-height: 0 !important;
134
+ display: flex !important;
135
+ flex-direction: column !important;
136
+ padding: 16px 24px 12px !important;
137
+ box-sizing: border-box !important;
138
+ overflow: hidden !important;
139
+ background: var(--weba-canvas) !important;
140
+ }
141
+ #app-shell > .gap, #app-shell > div {
142
+ background: transparent !important;
143
  }
 
 
 
144
 
145
  .twil-header {
146
  display: flex;
147
  align-items: center;
148
  gap: 12px;
149
+ padding: 4px 0 8px;
150
+ flex-shrink: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  }
152
+ .twil-mark { width: 28px; height: 28px; border-radius: 6px; }
153
+ .twil-wordmark { font-weight: 600; font-size: 15px; letter-spacing: -0.02em; line-height: 1.2; color: var(--weba-fg); }
154
+ .twil-product { font-size: 12px; color: var(--weba-muted); line-height: 1.3; }
155
+
156
  .twil-empty {
157
+ height: 100%;
158
+ min-height: 280px;
159
  display: flex;
160
  flex-direction: column;
161
  align-items: center;
162
+ justify-content: center;
163
+ text-align: center;
164
+ padding: 12px;
165
  }
166
+ .twil-cube { width: 96px; height: 96px; margin-bottom: 24px; }
 
 
 
 
167
  .twil-empty h1 {
168
  font-size: 28px;
169
  font-weight: 500;
170
  letter-spacing: -0.03em;
171
+ margin: 0 0 8px;
172
  color: var(--weba-fg);
173
  }
174
  .twil-empty p {
175
  margin: 0;
176
  max-width: 28rem;
177
+ font-size: 15px;
178
  line-height: 1.5;
179
  color: var(--weba-muted);
180
  }
 
 
 
 
 
 
 
181
 
182
+ #twil-chat, #twil-chat > .wrapper, #twil-chat .bubble-wrap,
183
+ #twil-chat .message-wrap, #twil-chat .placeholder-content {
184
  background: transparent !important;
185
  border: none !important;
186
  box-shadow: none !important;
187
+ flex: 1 1 auto !important;
188
+ min-height: 0 !important;
189
  }
190
+ #twil-chat {
191
+ max-width: 768px !important;
192
+ width: 100% !important;
193
+ margin: 0 auto !important;
194
  }
195
+ #twil-chat .message.user, #twil-chat .user, #twil-chat .bubble.user {
 
196
  background: var(--weba-secondary) !important;
197
  color: var(--weba-fg) !important;
198
  border: none !important;
199
+ box-shadow: none !important;
200
  border-radius: 12px !important;
201
  }
202
+ #twil-chat .message.bot, #twil-chat .bot, #twil-chat .bubble.bot {
 
203
  background: transparent !important;
204
  border: none !important;
205
  box-shadow: none !important;
206
  }
207
+ #twil-chat [aria-label="Delete"], #twil-chat [aria-label="Copy"],
208
+ #twil-chat [aria-label="Share"], #twil-chat .message-buttons {
209
+ display: none !important;
210
+ }
211
+ #twil-chat details, #twil-chat .thought, #twil-chat .md.thought {
212
+ background: transparent !important;
213
+ border: none !important;
214
+ box-shadow: none !important;
215
+ color: var(--weba-muted) !important;
216
+ }
217
 
218
+ #composer-row {
219
+ max-width: 768px !important;
220
+ width: 100% !important;
221
+ margin: 8px auto 0 !important;
222
+ flex-shrink: 0 !important;
223
+ background: transparent !important;
224
+ }
225
+ #chips-row {
226
+ max-width: 768px !important;
227
+ width: 100% !important;
228
+ margin: 0 auto !important;
229
+ display: flex !important;
230
+ flex-wrap: wrap !important;
231
+ justify-content: center !important;
232
+ gap: 8px !important;
233
+ flex-shrink: 0 !important;
234
+ background: transparent !important;
235
+ padding: 4px 0 8px !important;
236
+ }
237
+ #chips-row button {
238
+ height: 32px !important;
239
+ padding: 0 14px !important;
240
+ border-radius: 999px !important;
241
+ border: 1px solid var(--weba-border) !important;
242
+ background: transparent !important;
243
+ color: var(--weba-muted) !important;
244
+ box-shadow: none !important;
245
+ font-size: 13px !important;
246
+ font-weight: 400 !important;
247
+ }
248
+ #chips-row button:hover {
249
+ background: var(--weba-secondary) !important;
250
+ color: var(--weba-fg) !important;
251
+ }
252
+
253
+ #twil-input textarea {
254
  background: var(--weba-composer) !important;
255
  color: var(--weba-fg) !important;
256
  border: 1px solid var(--weba-border) !important;
257
  border-radius: 999px !important;
258
+ padding: 16px 18px !important;
259
  font-size: 16px !important;
260
  box-shadow: none !important;
261
+ resize: none !important;
262
  }
263
+ #twil-input, #twil-input > .wrap, #composer-row .form,
264
+ #composer-row .block, #composer-row .padded {
265
  background: transparent !important;
266
  border: none !important;
267
  box-shadow: none !important;
268
+ padding: 0 !important;
269
  }
270
 
271
+ #send-btn {
272
+ min-width: 40px !important;
273
+ width: 40px !important;
274
+ height: 40px !important;
275
  border-radius: 999px !important;
 
 
276
  background: var(--weba-secondary) !important;
277
  color: var(--weba-fg) !important;
278
+ border: none !important;
279
+ box-shadow: none !important;
280
+ align-self: center !important;
281
  }
282
 
283
+ #params-box {
284
+ max-width: 768px !important;
285
+ margin: 4px auto 0 !important;
 
 
 
 
 
 
 
 
286
  background: transparent !important;
287
+ border: none !important;
288
+ box-shadow: none !important;
289
+ flex-shrink: 0 !important;
290
+ }
291
+ #params-box .label-wrap, #params-box .icon {
292
  color: var(--weba-muted) !important;
293
+ font-size: 12px !important;
294
+ box-shadow: none !important;
295
+ background: transparent !important;
296
+ border: none !important;
297
+ }
298
+
299
+ .twil-disclaimer {
300
+ margin: 8px 0 0;
301
+ text-align: center;
302
+ font-size: 11px;
303
+ color: var(--weba-muted);
304
+ flex-shrink: 0;
305
  }
306
  """
307
 
 
310
  secondary_hue="zinc",
311
  neutral_hue="zinc",
312
  font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
 
313
  radius_size=gr.themes.sizes.radius_lg,
314
  ).set(
315
  body_background_fill="#f4f4f4",
316
  body_background_fill_dark="#232323",
317
  body_text_color="#161616",
318
  body_text_color_dark="#fafafa",
319
+ background_fill_primary="transparent",
320
+ background_fill_primary_dark="transparent",
321
  background_fill_secondary="#e8e8e8",
322
  background_fill_secondary_dark="#323232",
323
+ border_color_primary="rgba(22,22,22,0.1)",
324
  border_color_primary_dark="rgba(255,255,255,0.1)",
325
  block_background_fill="transparent",
326
  block_background_fill_dark="transparent",
327
  block_border_width="0px",
328
  block_shadow="none",
329
  block_shadow_dark="none",
330
+ shadow_drop="none",
331
+ shadow_drop_lg="none",
332
+ input_background_fill="#ffffff",
333
+ input_background_fill_dark="#161616",
334
+ input_border_color="rgba(22,22,22,0.1)",
335
+ input_border_color_dark="rgba(255,255,255,0.1)",
336
+ input_shadow="none",
337
  button_secondary_background_fill="#e8e8e8",
338
  button_secondary_background_fill_dark="#323232",
339
  button_secondary_text_color="#161616",
340
  button_secondary_text_color_dark="#fafafa",
341
+ button_secondary_shadow="none",
 
 
 
342
  )
343
 
344
+ tokenizer = None
345
+ model = None
346
+ EOS_TOKEN_ID = None
 
 
 
 
 
347
 
348
+ if not PREVIEW:
349
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
350
+ model = AutoModelForCausalLM.from_pretrained(
351
+ MODEL_ID,
352
+ dtype=torch.bfloat16,
353
+ attn_implementation="sdpa",
354
+ ).to("cuda")
355
+ model.eval()
356
+ model.generation_config.use_cache = True
357
+ EOS_TOKEN_ID = tokenizer.eos_token_id
358
 
359
 
360
  def _strip_thinking(text: str) -> str:
 
361
  if not isinstance(text, str):
362
  return ""
363
  return _THINK_RE.sub("", text).strip()
364
 
365
 
366
+ def _parse_think(raw: str) -> tuple[str, str, bool]:
367
+ """Return (think, answer, think_open) from a possibly streaming reply."""
368
+ open_idx = raw.find(THINK_OPEN)
369
+ if open_idx == -1:
370
+ return "", raw, False
371
+ rest = raw[open_idx + len(THINK_OPEN) :]
372
+ close_idx = rest.find(THINK_CLOSE)
373
+ before = raw[:open_idx]
374
+ if close_idx == -1:
375
+ return rest.strip(), before.strip(), True
376
+ think = rest[:close_idx].strip()
377
+ answer = (before + rest[close_idx + len(THINK_CLOSE) :]).strip()
378
+ return think, answer, False
379
+
380
+
381
+ def _gpu_seconds(message, history, max_new_tokens=2048, *args, **kwargs):
382
  tokens = int(max_new_tokens or 2048)
383
  return min(180, max(45, 25 + tokens // 18))
384
 
385
 
386
+ def _history_for_model(history: list) -> list[dict]:
 
 
 
 
 
 
 
 
387
  conversation = [{"role": "system", "content": SYSTEM_PROMPT}]
388
  for msg in history or []:
389
  role = msg.get("role", "user")
390
+ if role == "system" or msg.get("metadata"):
 
391
  continue
392
+ content = msg.get("content", "")
393
  if isinstance(content, list):
394
  content = ""
395
  if role == "assistant":
396
  content = _strip_thinking(content)
397
+ if content:
398
+ conversation.append({"role": role, "content": content})
399
+ return conversation
400
+
401
+
402
+ def _stream_tokens(conversation: list[dict], max_new_tokens: int, temperature: float, top_p: float, enable_thinking: bool) -> Iterator[str]:
403
+ if PREVIEW:
404
+ demo = (
405
+ f"{THINK_OPEN}\n"
406
+ "Check the premises, then the conclusion. The argument is a classic Barbara syllogism.\n"
407
+ f"{THINK_CLOSE}\n\n"
408
+ "Answer: entailment."
409
+ )
410
+ acc = ""
411
+ for ch in demo:
412
+ acc += ch
413
+ yield acc
414
+ return
415
 
416
  encoded = tokenizer.apply_chat_template(
417
  conversation,
 
425
  if input_ids.shape[1] > MAX_INPUT_TOKEN_LENGTH:
426
  input_ids = input_ids[:, -MAX_INPUT_TOKEN_LENGTH:]
427
  attention_mask = attention_mask[:, -MAX_INPUT_TOKEN_LENGTH:]
428
+ gr.Warning(f"Trimmed the conversation to the last {MAX_INPUT_TOKEN_LENGTH} tokens.")
 
 
429
 
430
  input_ids = input_ids.to(model.device)
431
  attention_mask = attention_mask.to(model.device)
 
432
  streamer = TextIteratorStreamer(
433
  tokenizer, timeout=30.0, skip_prompt=True, skip_special_tokens=True
434
  )
 
450
  generate_kwargs["top_p"] = float(top_p)
451
 
452
  Thread(target=model.generate, kwargs=generate_kwargs, daemon=True).start()
453
+ acc = ""
 
454
  for text in streamer:
455
+ acc += text
456
+ yield acc
457
+
458
+
459
+ def _generate(message, history, max_new_tokens, temperature, top_p, enable_thinking):
460
+ history = [dict(m) if isinstance(m, dict) else m for m in (history or [])]
461
+ history.append({"role": "user", "content": message})
462
+ conversation = _history_for_model(history)
463
+
464
+ think_msg = {
465
+ "role": "assistant",
466
+ "content": "",
467
+ "metadata": {"title": "Logicizing…", "status": "pending"},
468
+ }
469
+ answer_msg = {"role": "assistant", "content": ""}
470
+ history.append(think_msg)
471
+ yield history
472
+
473
+ for raw in _stream_tokens(
474
+ conversation, max_new_tokens, temperature, top_p, enable_thinking
475
+ ):
476
+ think, answer, think_open = _parse_think(raw)
477
+ if think or think_open:
478
+ think_msg["content"] = think
479
+ think_msg["metadata"] = {
480
+ "title": "Logicizing…" if think_open else "Logic",
481
+ "status": "pending" if think_open else "done",
482
+ }
483
+ if answer:
484
+ answer_msg["content"] = answer
485
+ if not any(m is answer_msg for m in history):
486
+ history.append(answer_msg)
487
+ elif not think_open and not think and raw.strip():
488
+ answer_msg["content"] = raw.strip()
489
+ if not any(m is answer_msg for m in history):
490
+ history.append(answer_msg)
491
+ yield history
492
+
493
+ if think_msg["content"]:
494
+ think_msg["metadata"] = {"title": "Logic", "status": "done"}
495
+ yield history
496
+ elif think_msg in history:
497
+ history.remove(think_msg)
498
+ yield history
499
 
500
+
501
+ @spaces.GPU(duration=_gpu_seconds)
502
+ def submit(message, history, max_new_tokens, temperature, top_p, enable_thinking):
503
+ message = (message or "").strip()
504
+ if not message:
505
+ yield "", history, gr.skip()
506
+ return
507
+ first = True
508
+ for updated in _generate(
509
+ message, history, max_new_tokens, temperature, top_p, enable_thinking
510
+ ):
511
+ if first:
512
+ yield "", updated, gr.update(visible=False)
513
+ first = False
514
+ else:
515
+ yield gr.skip(), updated, gr.skip()
516
+
517
+
518
+ with gr.Blocks(fill_height=True, fill_width=True, elem_id="app-root") as demo:
519
+ with gr.Column(elem_id="app-shell"):
520
  gr.HTML(HEADER)
521
+ chatbot = gr.Chatbot(
522
+ value=[],
523
+ placeholder=PLACEHOLDER,
524
+ label="",
525
+ show_label=False,
526
+ layout="bubble",
527
+ avatar_images=(None, str(AVATAR) if AVATAR.exists() else None),
528
+ allow_tags=["think"],
529
+ reasoning_tags=[(THINK_OPEN, THINK_CLOSE)],
530
+ buttons=None,
531
+ editable=False,
532
+ line_breaks=False,
533
+ elem_id="twil-chat",
534
+ scale=1,
535
+ height="100%",
536
+ )
537
+ with gr.Row(elem_id="chips-row") as chips_row:
538
+ chip_btns = [
539
+ gr.Button(label, variant="secondary", size="sm", elem_classes=["twil-chip"])
540
+ for label, _ in CHIPS
541
+ ]
542
+ with gr.Row(elem_id="composer-row"):
543
+ prompt = gr.Textbox(
544
  placeholder="Ask webAI anything…",
545
  show_label=False,
546
  container=False,
547
+ lines=1,
548
+ max_lines=6,
549
  elem_id="twil-input",
550
+ scale=8,
551
+ )
552
+ send = gr.Button("↑", elem_id="send-btn", scale=0)
553
+ with gr.Accordion("Parameters", open=False, elem_id="params-box"):
554
+ max_new_tokens = gr.Slider(256, 4096, value=2048, step=256, label="Max new tokens")
555
+ temperature = gr.Slider(0, 1.5, value=0, step=0.05, label="Temperature (0 = greedy)")
556
+ top_p = gr.Slider(0.1, 1.0, value=0.95, step=0.05, label="Top-p")
557
+ enable_thinking = gr.Checkbox(value=True, label="Enable thinking")
558
+ gr.HTML('<p class="twil-disclaimer">This is AI and it can make mistakes</p>')
559
+
560
+ inputs = [prompt, chatbot, max_new_tokens, temperature, top_p, enable_thinking]
561
+ send.click(submit, inputs, [prompt, chatbot, chips_row], concurrency_limit=1)
562
+ prompt.submit(submit, inputs, [prompt, chatbot, chips_row], concurrency_limit=1)
563
+ for btn, (_, template) in zip(chip_btns, CHIPS):
564
+ btn.click(lambda t=template: t, outputs=prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
 
566
  if __name__ == "__main__":
567
  demo.launch(
568
  theme=THEME,
569
  css=CSS,
570
  allowed_paths=[str(ASSETS), str(ASSETS.parent)],
571
+ ssr_mode=False,
572
  )