ravids commited on
Commit
e361d46
Β·
1 Parent(s): 0dffe8b

Start to merge app.py with the Granite switch playground

Browse files
Files changed (2) hide show
  1. app.py +734 -505
  2. requirements.txt +6 -0
app.py CHANGED
@@ -1,567 +1,796 @@
1
- import os
2
- import uuid
3
- from enum import Enum
4
- from html import escape
5
- from typing import Optional
6
 
7
- from fastapi import FastAPI, Form, Header, HTTPException
8
- from fastapi.responses import HTMLResponse, RedirectResponse
9
- from pydantic import BaseModel
10
 
 
 
 
 
 
 
11
 
12
- # =============================================================================
13
- # Configuration
14
- # =============================================================================
15
 
16
- BROKER_TOKEN = os.environ.get("BROKER_TOKEN")
17
- UI_TOKEN = os.environ.get("UI_TOKEN")
18
 
19
- if not BROKER_TOKEN:
20
- raise RuntimeError(
21
- "Missing BROKER_TOKEN. Add it in Hugging Face Space "
22
- "Settings β†’ Variables and secrets β†’ New secret."
23
- )
24
-
25
- if not UI_TOKEN:
26
- raise RuntimeError(
27
- "Missing UI_TOKEN. Add it in Hugging Face Space "
28
- "Settings β†’ Variables and secrets β†’ New secret."
29
- )
30
 
31
-
32
- # =============================================================================
33
- # Safe predefined options
34
- # =============================================================================
35
 
36
  MODEL_OPTIONS = {
 
37
  "granite-4.1-8b": "ibm-granite/granite-4.1-8b",
38
  "granite-4.1-30b": "ibm-granite/granite-4.1-30b",
39
- "llama-3-3-70b-instruct": "meta-llama/llama-3-3-70b-instruct",
40
  }
41
 
42
- BASIC_COMMANDS = {
43
- "hostname": "Run hostname",
44
- "whoami": "Run whoami",
45
- "pwd": "Show current directory",
46
- "disk": "Show disk usage",
47
- "date": "Show current date",
48
- "list_home": "List home directory",
49
  }
50
-
51
- PARAMETERIZED_COMMANDS = {
52
- "query_llm": "Query LLM with model, GPU count, and prompt",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  }
54
 
 
 
55
 
56
- # =============================================================================
57
- # Data model
58
- # =============================================================================
59
-
60
- class JobStatus(str, Enum):
61
- queued = "queued"
62
- running = "running"
63
- done = "done"
64
- failed = "failed"
65
-
66
-
67
- class Job(BaseModel):
68
- id: str
69
- command: str
70
- status: JobStatus = JobStatus.queued
71
- result: Optional[str] = None
72
-
73
- # Parameters for query_llm
74
- model: Optional[str] = None
75
- gpus: Optional[int] = None
76
- user_text: Optional[str] = None
77
-
78
-
79
- app = FastAPI(title="Fury Broker")
80
-
81
- # In-memory storage. Jobs disappear if the Space restarts.
82
- jobs: dict[str, Job] = {}
83
-
84
-
85
- # =============================================================================
86
- # Security helpers
87
- # =============================================================================
88
-
89
- def verify_broker_token(x_broker_token: Optional[str]) -> None:
90
- """
91
- Used by the Fury worker and command-line API calls.
92
 
93
- Header:
94
- X-Broker-Token: BROKER_TOKEN
95
- """
96
- if x_broker_token != BROKER_TOKEN:
97
- raise HTTPException(status_code=401, detail="Unauthorized")
98
-
99
-
100
- def verify_ui_token(ui_token: Optional[str]) -> None:
101
- """
102
- Used by browser form submissions.
103
-
104
- Form field:
105
- ui_token
106
- """
107
- if ui_token != UI_TOKEN:
108
- raise HTTPException(status_code=401, detail="Invalid UI token")
109
-
110
-
111
- def validate_basic_command(command: str) -> None:
112
- if command not in BASIC_COMMANDS:
113
- raise HTTPException(status_code=400, detail=f"Command is not allowed: {command}")
114
 
115
 
116
- def validate_query_llm_args(
117
- model: str,
118
- gpus: int,
119
- user_text: str,
120
- ) -> None:
121
- if model not in MODEL_OPTIONS:
122
- raise HTTPException(status_code=400, detail=f"Model is not allowed: {model}")
123
 
124
  if gpus < 1 or gpus > 16:
125
- raise HTTPException(status_code=400, detail="GPUs must be between 1 and 16")
126
-
127
- if user_text is None:
128
- raise HTTPException(status_code=400, detail="Prompt is required")
129
 
130
- if len(user_text.strip()) == 0:
131
- raise HTTPException(status_code=400, detail="Prompt cannot be empty")
132
 
133
  if len(user_text) > 10_000:
134
- raise HTTPException(
135
- status_code=400,
136
- detail="Prompt is too long; max 10,000 characters",
137
- )
138
 
139
 
140
- # =============================================================================
141
- # Health and inspection APIs
142
- # =============================================================================
143
-
144
- @app.get("/health")
145
- def health():
146
- return {
147
- "status": "ok",
148
- "service": "fury-broker",
149
- "jobs_count": len(jobs),
150
- }
151
-
152
-
153
- @app.get("/api/commands")
154
- def list_commands(x_broker_token: Optional[str] = Header(default=None)):
155
- verify_broker_token(x_broker_token)
156
-
157
- return {
158
- "basic_commands": BASIC_COMMANDS,
159
- "parameterized_commands": PARAMETERIZED_COMMANDS,
160
- "query_llm": {
161
- "command": "query_llm",
162
- "model_options": MODEL_OPTIONS,
163
- "gpu_range": [1, 16],
164
- "default_gpus": 1,
165
- "default_prompt": "hello",
166
- },
167
- }
168
-
169
 
170
- # =============================================================================
171
- # Browser UI
172
- # =============================================================================
173
 
174
- @app.get("/", response_class=HTMLResponse)
175
- def home():
176
- basic_command_options_html = "\n".join(
177
- f'<option value="{escape(name)}">{escape(name)} β€” {escape(label)}</option>'
178
- for name, label in BASIC_COMMANDS.items()
179
- )
180
 
181
- model_options_html = "\n".join(
182
- f'<option value="{escape(key)}">{escape(value)}</option>'
183
- for key, value in MODEL_OPTIONS.items()
184
  )
185
-
186
- gpu_options_html = "\n".join(
187
- f'<option value="{i}" {"selected" if i == 1 else ""}>{i}</option>'
188
- for i in range(1, 17)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  )
190
-
191
- rows = ""
192
-
193
- for job in reversed(list(jobs.values())):
194
- details = []
195
-
196
- if job.model:
197
- details.append(f"model={job.model}")
198
-
199
- if job.gpus:
200
- details.append(f"gpus={job.gpus}")
201
-
202
- if job.user_text:
203
- preview = job.user_text[:500]
204
- if len(job.user_text) > 500:
205
- preview += "..."
206
- details.append(f"prompt={preview}")
207
-
208
- safe_details = escape("\n".join(details))
209
- safe_result = escape(job.result or "")
210
-
211
- rows += f"""
212
- <tr>
213
- <td><code>{escape(job.id)}</code></td>
214
- <td>{escape(job.command)}</td>
215
- <td>{escape(job.status.value)}</td>
216
- <td><pre>{safe_details}</pre></td>
217
- <td><pre>{safe_result}</pre></td>
218
- </tr>
219
- """
220
-
221
- return f"""
222
- <!doctype html>
223
- <html>
224
- <head>
225
- <title>Fury Broker</title>
226
- <style>
227
- body {{
228
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
229
- margin: 40px;
230
- max-width: 1300px;
231
- line-height: 1.45;
232
- }}
233
- h1 {{
234
- margin-bottom: 0.2rem;
235
- }}
236
- .subtitle {{
237
- color: #555;
238
- margin-bottom: 2rem;
239
- }}
240
- form {{
241
- margin: 1.5rem 0;
242
- padding: 1rem;
243
- border: 1px solid #ddd;
244
- border-radius: 8px;
245
- background: #fafafa;
246
- }}
247
- input, select, textarea, button {{
248
- padding: 6px;
249
- margin: 4px;
250
- }}
251
- textarea {{
252
- width: 95%;
253
- font-family: monospace;
254
- }}
255
- table {{
256
- border-collapse: collapse;
257
- width: 100%;
258
- margin-top: 20px;
259
- }}
260
- th, td {{
261
- border: 1px solid #ccc;
262
- padding: 8px;
263
- vertical-align: top;
264
- }}
265
- th {{
266
- background: #f5f5f5;
267
- text-align: left;
268
- }}
269
- pre {{
270
- white-space: pre-wrap;
271
- max-width: 600px;
272
- max-height: 500px;
273
- overflow: auto;
274
- margin: 0;
275
- }}
276
- .warning {{
277
- color: #8a4b00;
278
- background: #fff4dd;
279
- border: 1px solid #f0c36d;
280
- padding: 0.8rem;
281
- border-radius: 6px;
282
- }}
283
- </style>
284
- </head>
285
- <body>
286
- <h1>Fury Broker</h1>
287
- <div class="subtitle">
288
- Submit approved jobs from Hugging Face Spaces to the worker running on Fury.
289
- </div>
290
-
291
- <div class="warning">
292
- This UI does not allow arbitrary shell commands.
293
- It submits only predefined command names and validated parameters.
294
- Fury validates the job again locally before execution.
295
- </div>
296
-
297
- <form method="post" action="/submit-basic">
298
- <h2>Basic Fury Command</h2>
299
-
300
- <div>
301
- <label><strong>UI token:</strong></label>
302
- <input type="password" name="ui_token" placeholder="Enter UI_TOKEN" required>
303
- </div>
304
-
305
- <div>
306
- <label><strong>Command:</strong></label>
307
- <select name="command">
308
- {basic_command_options_html}
309
- </select>
310
- </div>
311
-
312
- <button type="submit">Submit basic job</button>
313
- </form>
314
-
315
- <form method="post" action="/submit-query-llm">
316
- <h2>query_llm</h2>
317
-
318
- <div>
319
- <label><strong>UI token:</strong></label>
320
- <input type="password" name="ui_token" placeholder="Enter UI_TOKEN" required>
321
- </div>
322
-
323
- <div>
324
- <label><strong>Model:</strong></label>
325
- <select name="model">
326
- {model_options_html}
327
- </select>
328
- </div>
329
-
330
- <div>
331
- <label><strong>Number of GPUs:</strong></label>
332
- <select name="gpus">
333
- {gpu_options_html}
334
- </select>
335
- </div>
336
-
337
- <div>
338
- <label><strong>Prompt to send to the LLM:</strong></label><br>
339
- <textarea
340
- name="user_text"
341
- rows="8"
342
- required
343
- >hello</textarea>
344
- </div>
345
-
346
- <button type="submit">Submit query_llm job</button>
347
- </form>
348
-
349
- <p>
350
- Refresh the page after a few seconds to see updated results.
351
- </p>
352
-
353
- <h2>Jobs</h2>
354
-
355
- <table>
356
- <tr>
357
- <th>ID</th>
358
- <th>Command</th>
359
- <th>Status</th>
360
- <th>Parameters</th>
361
- <th>Result</th>
362
- </tr>
363
- {rows}
364
- </table>
365
- </body>
366
- </html>
367
- """
368
-
369
-
370
- # =============================================================================
371
- # Browser submit endpoints
372
- # =============================================================================
373
-
374
- @app.post("/submit-basic")
375
- def submit_basic_from_ui(
376
- command: str = Form(...),
377
- ui_token: str = Form(...),
378
- ):
379
- verify_ui_token(ui_token)
380
- validate_basic_command(command)
381
-
382
- job_id = str(uuid.uuid4())
383
-
384
- jobs[job_id] = Job(
385
- id=job_id,
386
- command=command,
387
- status=JobStatus.queued,
388
  )
389
 
390
- return RedirectResponse("/", status_code=303)
391
 
 
 
 
 
 
 
392
 
393
- @app.post("/submit-query-llm")
394
- def submit_query_llm_from_ui(
395
- model: str = Form(...),
396
- gpus: int = Form(1),
397
- user_text: str = Form("hello"),
398
- ui_token: str = Form(...),
399
- ):
400
- verify_ui_token(ui_token)
401
-
402
- validate_query_llm_args(
403
- model=model,
404
- gpus=gpus,
405
- user_text=user_text,
406
  )
407
 
408
- job_id = str(uuid.uuid4())
409
-
410
- jobs[job_id] = Job(
411
- id=job_id,
412
- command="query_llm",
413
- model=model,
414
- gpus=gpus,
415
- user_text=user_text,
416
- status=JobStatus.queued,
417
- )
418
 
419
- return RedirectResponse("/", status_code=303)
 
 
 
 
 
 
 
 
 
 
420
 
421
 
422
- # =============================================================================
423
- # API submit endpoints
424
- # =============================================================================
 
 
 
425
 
426
- @app.post("/api/jobs/basic")
427
- def submit_basic_job_api(
428
- command: str = Form(...),
429
- x_broker_token: Optional[str] = Header(default=None),
430
- ):
431
- verify_broker_token(x_broker_token)
432
- validate_basic_command(command)
433
 
434
- job_id = str(uuid.uuid4())
 
 
435
 
436
- job = Job(
437
- id=job_id,
438
- command=command,
439
- status=JobStatus.queued,
440
- )
441
 
442
- jobs[job_id] = job
443
- return job
 
 
444
 
445
 
446
- @app.post("/api/jobs/query-llm")
447
- def submit_query_llm_job_api(
448
- model: str = Form(...),
449
- gpus: int = Form(1),
450
- user_text: str = Form("hello"),
451
- x_broker_token: Optional[str] = Header(default=None),
452
- ):
453
- verify_broker_token(x_broker_token)
454
 
455
- validate_query_llm_args(
456
- model=model,
457
- gpus=gpus,
458
- user_text=user_text,
459
- )
460
 
461
- job_id = str(uuid.uuid4())
 
 
 
 
462
 
463
- job = Job(
464
- id=job_id,
465
- command="query_llm",
466
- model=model,
467
- gpus=gpus,
468
- user_text=user_text,
469
- status=JobStatus.queued,
470
  )
471
-
472
- jobs[job_id] = job
473
- return job
474
-
475
-
476
- # Backward-compatible endpoint for simple jobs.
477
- @app.post("/api/jobs")
478
- def submit_job_legacy_api(
479
- command: str = Form(...),
480
- x_broker_token: Optional[str] = Header(default=None),
481
- ):
482
- verify_broker_token(x_broker_token)
483
- validate_basic_command(command)
484
-
485
- job_id = str(uuid.uuid4())
486
-
487
- job = Job(
488
- id=job_id,
489
- command=command,
490
- status=JobStatus.queued,
491
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
 
493
- jobs[job_id] = job
494
- return job
495
-
496
-
497
- # =============================================================================
498
- # Worker polling and result posting
499
- # =============================================================================
500
-
501
- @app.get("/api/next-job")
502
- def get_next_job(x_broker_token: Optional[str] = Header(default=None)):
503
- verify_broker_token(x_broker_token)
504
-
505
- for job in jobs.values():
506
- if job.status == JobStatus.queued:
507
- job.status = JobStatus.running
508
-
509
- return {
510
- "id": job.id,
511
- "command": job.command,
512
- "model": job.model,
513
- "gpus": job.gpus,
514
- "user_text": job.user_text,
515
- }
516
-
517
- return {"id": None}
518
-
519
-
520
- @app.post("/api/jobs/{job_id}/result")
521
- def post_result(
522
- job_id: str,
523
- result: str = Form(...),
524
- success: bool = Form(...),
525
- x_broker_token: Optional[str] = Header(default=None),
526
- ):
527
- verify_broker_token(x_broker_token)
528
-
529
- if job_id not in jobs:
530
- raise HTTPException(status_code=404, detail="Job not found")
531
-
532
- job = jobs[job_id]
533
- job.result = result
534
- job.status = JobStatus.done if success else JobStatus.failed
535
-
536
- return job
537
 
 
 
 
538
 
539
- @app.get("/api/jobs")
540
- def list_jobs(x_broker_token: Optional[str] = Header(default=None)):
541
- verify_broker_token(x_broker_token)
542
- return {"jobs": list(jobs.values())}
543
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
 
545
- @app.get("/api/jobs/{job_id}")
546
- def get_job(
547
- job_id: str,
548
- x_broker_token: Optional[str] = Header(default=None),
549
- ):
550
- verify_broker_token(x_broker_token)
 
 
 
 
551
 
552
- if job_id not in jobs:
553
- raise HTTPException(status_code=404, detail="Job not found")
554
 
555
- return jobs[job_id]
 
 
556
 
557
 
558
- @app.post("/api/clear")
559
- def clear_jobs(x_broker_token: Optional[str] = Header(default=None)):
560
- verify_broker_token(x_broker_token)
 
 
 
 
 
561
 
562
- jobs.clear()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
563
 
564
- return {
565
- "status": "cleared",
566
- "jobs_count": len(jobs),
567
- }
 
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Granite Switch 4.1 3B Playground β€” Hugging Face Space.
 
 
4
 
5
+ Each adapter has a specific prompt protocol. This app provides structured
6
+ input forms per adapter so the control tokens AND prompt formats are correct.
7
+ """
8
 
9
+ import json
10
+ import os
11
+ import time
12
+ import urllib.error
13
+ import urllib.parse
14
+ import urllib.request
15
 
16
+ import spaces
17
+ import torch
 
18
 
19
+ import granite_switch.hf # noqa: F401 β€” registers HF backend
 
20
 
21
+ import gradio as gr
22
+ from transformers import AutoModelForCausalLM, AutoTokenizer
 
 
 
 
 
 
 
 
 
23
 
24
+ MODEL_ID = "ibm-granite/granite-switch-4.1-3b-preview"
 
 
 
25
 
26
  MODEL_OPTIONS = {
27
+ "granite-4.0-micro": "ibm-granite/granite-4.0-micro",
28
  "granite-4.1-8b": "ibm-granite/granite-4.1-8b",
29
  "granite-4.1-30b": "ibm-granite/granite-4.1-30b",
30
+ "qwen2.5-coder-32b": "Qwen/Qwen2.5-Coder-32B-Instruct",
31
  }
32
 
33
+ USE_EXTERNAL_LLM = os.getenv("USE_EXTERNAL_LLM", "").lower() in {
34
+ "1",
35
+ "true",
36
+ "yes",
37
+ "on",
 
 
38
  }
39
+ EXTERNAL_LLM_MODEL = os.getenv("EXTERNAL_LLM_MODEL", "granite-4.0-micro")
40
+ EXTERNAL_LLM_GPUS = int(os.getenv("EXTERNAL_LLM_GPUS", "1"))
41
+ EXTERNAL_LLM_BROKER_URL = os.getenv("EXTERNAL_LLM_BROKER_URL") or os.getenv("BROKER_URL")
42
+ EXTERNAL_LLM_TIMEOUT_SECONDS = int(os.getenv("EXTERNAL_LLM_TIMEOUT_SECONDS", "600"))
43
+ EXTERNAL_LLM_POLL_INTERVAL_SECONDS = float(
44
+ os.getenv("EXTERNAL_LLM_POLL_INTERVAL_SECONDS", "2")
45
+ )
46
+
47
+ JUDGE_SYSTEM = (
48
+ "As a judge agent, your role is to help assess whether the provided text "
49
+ "meets the given judging criteria, utilizing all available information, "
50
+ "including conversations, documents, and tools."
51
+ )
52
+
53
+ GUARDIAN_CRITERIA_BANK = {
54
+ "harm": (
55
+ "Texts deemed universally harmful are those that pose harm across all "
56
+ "contexts and audiences."
57
+ ),
58
+ "social_bias": (
59
+ "Social bias refers to prejudice or discrimination against individuals "
60
+ "or groups based on social characteristics such as race, gender, "
61
+ "religion, or socioeconomic status."
62
+ ),
63
+ "jailbreak": (
64
+ "Jailbreaking: the deliberate evasion of an AI system's built-in "
65
+ "safeguards or ethical boundaries, often by crafting specific prompts "
66
+ "or scenarios intended to manipulate the AI into producing restricted "
67
+ "or inappropriate content."
68
+ ),
69
+ "profanity": "The text contains profane or vulgar language.",
70
+ "violence": "The text contains descriptions of or encouragement of violence.",
71
+ "unethical_behavior": "The text promotes unethical behavior or actions.",
72
  }
73
 
74
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
75
+ model = None
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
+ def _get_model():
79
+ global model
80
+ if model is None:
81
+ model = AutoModelForCausalLM.from_pretrained(
82
+ MODEL_ID, torch_dtype=torch.bfloat16
83
+ )
84
+ model.eval()
85
+ model.to("cuda")
86
+ return model
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
 
89
+ def validate_query_llm_args(model_name, gpus, user_text):
90
+ if model_name not in MODEL_OPTIONS:
91
+ raise ValueError(f"Model is not allowed: {model_name}")
 
 
 
 
92
 
93
  if gpus < 1 or gpus > 16:
94
+ raise ValueError("GPUs must be between 1 and 16")
 
 
 
95
 
96
+ if user_text is None or not user_text.strip():
97
+ raise ValueError("Prompt cannot be empty")
98
 
99
  if len(user_text) > 10_000:
100
+ raise ValueError("Prompt is too long; max 10,000 characters")
 
 
 
101
 
102
 
103
+ def _broker_request(path, data=None, method="GET"):
104
+ if not EXTERNAL_LLM_BROKER_URL:
105
+ raise RuntimeError(
106
+ "USE_EXTERNAL_LLM is set, but EXTERNAL_LLM_BROKER_URL or BROKER_URL "
107
+ "is missing."
108
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
+ broker_token = os.getenv("BROKER_TOKEN")
111
+ if not broker_token:
112
+ raise RuntimeError("USE_EXTERNAL_LLM is set, but BROKER_TOKEN is missing.")
113
 
114
+ url = f"{EXTERNAL_LLM_BROKER_URL.rstrip('/')}/{path.lstrip('/')}"
115
+ encoded_data = None
116
+ headers = {"X-Broker-Token": broker_token}
117
+ if data is not None:
118
+ encoded_data = urllib.parse.urlencode(data).encode("utf-8")
119
+ headers["Content-Type"] = "application/x-www-form-urlencoded"
120
 
121
+ request = urllib.request.Request(
122
+ url, data=encoded_data, headers=headers, method=method
 
123
  )
124
+ try:
125
+ with urllib.request.urlopen(request, timeout=60) as response:
126
+ return json.loads(response.read().decode("utf-8"))
127
+ except urllib.error.HTTPError as exc:
128
+ detail = exc.read().decode("utf-8", errors="replace")
129
+ raise RuntimeError(f"Broker returned HTTP {exc.code}: {detail}") from exc
130
+ except urllib.error.URLError as exc:
131
+ raise RuntimeError(f"Could not connect to broker: {exc.reason}") from exc
132
+
133
+
134
+ def query_llm(user_text, max_new_tokens=128):
135
+ """Submit a query_llm job to the broker and wait for the worker result."""
136
+ validate_query_llm_args(EXTERNAL_LLM_MODEL, EXTERNAL_LLM_GPUS, user_text)
137
+
138
+ job = _broker_request(
139
+ "/api/jobs/query-llm",
140
+ data={
141
+ "model": EXTERNAL_LLM_MODEL,
142
+ "gpus": str(EXTERNAL_LLM_GPUS),
143
+ "user_text": user_text,
144
+ },
145
+ method="POST",
146
  )
147
+ job_id = job["id"]
148
+ deadline = time.monotonic() + EXTERNAL_LLM_TIMEOUT_SECONDS
149
+
150
+ while time.monotonic() < deadline:
151
+ job = _broker_request(f"/api/jobs/{job_id}")
152
+ status = job.get("status")
153
+ if status == "done":
154
+ return (job.get("result") or "").strip()
155
+ if status == "failed":
156
+ raise RuntimeError(job.get("result") or f"query_llm job {job_id} failed")
157
+ time.sleep(EXTERNAL_LLM_POLL_INTERVAL_SECONDS)
158
+
159
+ raise TimeoutError(
160
+ f"Timed out waiting for query_llm job {job_id} after "
161
+ f"{EXTERNAL_LLM_TIMEOUT_SECONDS} seconds"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  )
163
 
 
164
 
165
+ def _render_prompt(messages, adapter=None, documents=None):
166
+ kwargs = {}
167
+ if adapter:
168
+ kwargs["adapter_name"] = adapter
169
+ if documents:
170
+ kwargs["documents"] = documents
171
 
172
+ return tokenizer.apply_chat_template(
173
+ messages, add_generation_prompt=True, tokenize=False, **kwargs
 
 
 
 
 
 
 
 
 
 
 
174
  )
175
 
 
 
 
 
 
 
 
 
 
 
176
 
177
+ @spaces.GPU
178
+ def _generate_local(prompt, max_new_tokens=128):
179
+ m = _get_model()
180
+ inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
181
+ with torch.no_grad():
182
+ output_ids = m.generate(
183
+ **inputs, max_new_tokens=max_new_tokens, do_sample=False
184
+ )
185
+ return tokenizer.decode(
186
+ output_ids[0][inputs["input_ids"].shape[1] :], skip_special_tokens=True
187
+ ).strip()
188
 
189
 
190
+ def _generate(messages, adapter=None, documents=None, max_new_tokens=128):
191
+ """Core generation: render chat template, then use local or external LLM."""
192
+ prompt = _render_prompt(messages, adapter=adapter, documents=documents)
193
+ if USE_EXTERNAL_LLM:
194
+ return query_llm(prompt, max_new_tokens=max_new_tokens)
195
+ return _generate_local(prompt, max_new_tokens=max_new_tokens)
196
 
 
 
 
 
 
 
 
197
 
198
+ # ---------------------------------------------------------------------------
199
+ # Tab: Free Chat (base model, no adapter)
200
+ # ---------------------------------------------------------------------------
201
 
 
 
 
 
 
202
 
203
+ def chat_base(message, history, documents, max_tokens):
204
+ messages = list(history) + [{"role": "user", "content": message}]
205
+ docs = [{"text": documents.strip()}] if documents and documents.strip() else None
206
+ return _generate(messages, adapter=None, documents=docs, max_new_tokens=max_tokens)
207
 
208
 
209
+ # ---------------------------------------------------------------------------
210
+ # Tab: Guardian (harm, bias, jailbreak detection)
211
+ # ---------------------------------------------------------------------------
 
 
 
 
 
212
 
 
 
 
 
 
213
 
214
+ def run_guardian(text_to_evaluate, criteria_choice, custom_criteria, max_tokens):
215
+ if criteria_choice == "Custom":
216
+ criteria = custom_criteria
217
+ else:
218
+ criteria = GUARDIAN_CRITERIA_BANK[criteria_choice]
219
 
220
+ schema = (
221
+ "If the last user's text meets the criteria, return 'yes'; "
222
+ "otherwise, return 'no'."
 
 
 
 
223
  )
224
+ guardian_block = (
225
+ f"<guardian>{JUDGE_SYSTEM}\n\n"
226
+ f"### Criteria: {criteria}\n\n"
227
+ f"### Scoring Schema: {schema}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  )
229
+ messages = [
230
+ {"role": "user", "content": text_to_evaluate},
231
+ {"role": "user", "content": guardian_block},
232
+ ]
233
+ raw = _generate(messages, adapter="guardian-core", max_new_tokens=max_tokens)
234
+ try:
235
+ result = json.loads(raw)
236
+ score = result.get("score", raw)
237
+ return f"**Score:** `{score}`\n\n**Raw output:** `{raw}`"
238
+ except json.JSONDecodeError:
239
+ return f"**Raw output:** `{raw}`"
240
+
241
+
242
+ # ---------------------------------------------------------------------------
243
+ # Tab: Query Rewrite
244
+ # ---------------------------------------------------------------------------
245
+
246
+
247
+ def run_query_rewrite(query, max_tokens):
248
+ messages = [{"role": "user", "content": query}]
249
+ raw = _generate(messages, adapter="query_rewrite", max_new_tokens=max_tokens)
250
+ return f"**Rewritten query:** {raw}"
251
+
252
+
253
+ # ---------------------------------------------------------------------------
254
+ # Tab: Answerability
255
+ # ---------------------------------------------------------------------------
256
+
257
+
258
+ def run_answerability(question, documents, max_tokens):
259
+ docs = [{"text": d.strip()} for d in documents.split("\n---\n") if d.strip()]
260
+ messages = [{"role": "user", "content": question}]
261
+ raw = _generate(messages, adapter="answerability", documents=docs, max_new_tokens=max_tokens)
262
+ return f"**Result:** {raw}"
263
+
264
+
265
+ # ---------------------------------------------------------------------------
266
+ # Tab: Citations
267
+ # ---------------------------------------------------------------------------
268
+
269
+
270
+ def run_citations(question, answer, documents, max_tokens):
271
+ docs = [{"text": d.strip()} for d in documents.split("\n---\n") if d.strip()]
272
+ messages = [
273
+ {"role": "user", "content": question},
274
+ {"role": "assistant", "content": answer},
275
+ ]
276
+ raw = _generate(messages, adapter="citations", documents=docs, max_new_tokens=max_tokens)
277
+ return f"**Citations:** {raw}"
278
+
279
+
280
+ # ---------------------------------------------------------------------------
281
+ # Tab: Hallucination Detection
282
+ # ---------------------------------------------------------------------------
283
+
284
+
285
+ def run_hallucination_detection(question, answer, documents, max_tokens):
286
+ docs = [{"text": d.strip()} for d in documents.split("\n---\n") if d.strip()]
287
+ messages = [
288
+ {"role": "user", "content": question},
289
+ {"role": "assistant", "content": answer},
290
+ ]
291
+ raw = _generate(messages, adapter="hallucination_detection", documents=docs, max_new_tokens=max_tokens)
292
+ return f"**Result:** {raw}"
293
+
294
+
295
+ # ---------------------------------------------------------------------------
296
+ # Tab: Uncertainty
297
+ # ---------------------------------------------------------------------------
298
+
299
+
300
+ def run_uncertainty(conversation_text, max_tokens):
301
+ messages = [
302
+ {"role": "user", "content": conversation_text},
303
+ {"role": "user", "content": "<certainty>"},
304
+ ]
305
+ raw = _generate(messages, adapter="uncertainty", max_new_tokens=max_tokens)
306
+ try:
307
+ result = json.loads(raw)
308
+ digit = int(result.get("score", 0))
309
+ prob = 0.1 * digit + 0.05
310
+ return (
311
+ f"**Certainty digit:** `{digit}`\n\n"
312
+ f"**Calibrated probability:** ~{prob*100:.0f}%\n\n"
313
+ f"**Raw output:** `{raw}`"
314
+ )
315
+ except (json.JSONDecodeError, ValueError):
316
+ return f"**Raw output:** `{raw}`"
317
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
 
319
+ # ---------------------------------------------------------------------------
320
+ # Tab: Requirement Check
321
+ # ---------------------------------------------------------------------------
322
 
 
 
 
 
323
 
324
+ def run_requirement_check(user_question, assistant_response, requirements, max_tokens):
325
+ evaluation_prompt = (
326
+ "Please verify if the assistant's generation satisfies the user's "
327
+ "requirements or not and reply with a binary label accordingly. "
328
+ 'Respond with a json {"score": "yes"} if the constraints are satisfied '
329
+ 'or respond with {"score": "no"} if the constraints are not satisfied.'
330
+ )
331
+ req_turn = f"<requirements> {requirements}\n{evaluation_prompt}"
332
+
333
+ messages = [
334
+ {"role": "user", "content": user_question},
335
+ {"role": "assistant", "content": assistant_response},
336
+ {"role": "user", "content": req_turn},
337
+ ]
338
+ raw = _generate(messages, adapter="requirement-check", max_new_tokens=max_tokens)
339
+ try:
340
+ result = json.loads(raw)
341
+ score = result.get("score", raw)
342
+ label = "Satisfied" if score == "yes" else "Not satisfied"
343
+ return f"**{label}** (`{score}`)\n\n**Raw output:** `{raw}`"
344
+ except json.JSONDecodeError:
345
+ return f"**Raw output:** `{raw}`"
346
+
347
+
348
+ # ---------------------------------------------------------------------------
349
+ # Tab: Factuality Detection
350
+ # ---------------------------------------------------------------------------
351
+
352
+
353
+ def run_factuality_detection(assistant_response, documents, max_tokens):
354
+ docs = [{"text": d.strip()} for d in documents.split("\n---\n") if d.strip()]
355
+
356
+ factuality_criteria = (
357
+ "A factually incorrect response occurs when the assistant's message "
358
+ "contains one or more factual claims that are unsupported by, "
359
+ "inconsistent with, or directly contradicted by the information "
360
+ "provided in the documents or context."
361
+ )
362
+ schema = (
363
+ "If the last assistant's text meets the criteria, return 'yes'; "
364
+ "otherwise, return 'no'."
365
+ )
366
+ guardian_block = (
367
+ f"<guardian>{JUDGE_SYSTEM}\n\n"
368
+ f"### Criteria: {factuality_criteria}\n\n"
369
+ f"### Scoring Schema: {schema}"
370
+ )
371
+ messages = [
372
+ {"role": "assistant", "content": assistant_response},
373
+ {"role": "user", "content": guardian_block},
374
+ ]
375
+ raw = _generate(messages, adapter="factuality-detection", documents=docs, max_new_tokens=max_tokens)
376
+ try:
377
+ result = json.loads(raw)
378
+ score = result.get("score", raw)
379
+ label = "Factual errors found" if score == "yes" else "No errors detected"
380
+ return f"**{label}** (`{score}`)\n\n**Raw output:** `{raw}`"
381
+ except json.JSONDecodeError:
382
+ return f"**Raw output:** `{raw}`"
383
+
384
+
385
+ # ---------------------------------------------------------------------------
386
+ # Tab: Factuality Correction
387
+ # ---------------------------------------------------------------------------
388
+
389
+
390
+ def run_factuality_correction(assistant_response, documents, max_tokens):
391
+ docs = [{"text": d.strip()} for d in documents.split("\n---\n") if d.strip()]
392
+
393
+ factuality_criteria = (
394
+ "A factually incorrect response occurs when the assistant's message "
395
+ "contains one or more factual claims that are unsupported by, "
396
+ "inconsistent with, or directly contradicted by the information "
397
+ "provided in the documents or context."
398
+ )
399
+ schema = (
400
+ "If the last assistant's text meets the criteria, return a corrected "
401
+ "version of the assistant's message based on the given context; "
402
+ "otherwise, return 'none'."
403
+ )
404
+ guardian_block = (
405
+ f"<guardian>{JUDGE_SYSTEM}\n\n"
406
+ f"### Criteria: {factuality_criteria}\n\n"
407
+ f"### Scoring Schema: {schema}"
408
+ )
409
+ messages = [
410
+ {"role": "assistant", "content": assistant_response},
411
+ {"role": "user", "content": guardian_block},
412
+ ]
413
+ raw = _generate(messages, adapter="factuality-correction", documents=docs, max_new_tokens=max_tokens)
414
+ try:
415
+ result = json.loads(raw)
416
+ correction = result.get("correction", raw)
417
+ if correction == "none":
418
+ return "**No correction needed.**\n\n" + f"**Raw output:** `{raw}`"
419
+ return f"**Corrected text:**\n\n{correction}\n\n**Raw output:** `{raw}`"
420
+ except json.JSONDecodeError:
421
+ return f"**Raw output:** `{raw}`"
422
+
423
+
424
+ # ---------------------------------------------------------------------------
425
+ # Tab: Policy Guardrails
426
+ # ---------------------------------------------------------------------------
427
+
428
+
429
+ def run_policy_guardrails(scenario, policy, max_tokens):
430
+ policy_system = (
431
+ "You are a compliance agent trying to help determine whether a "
432
+ "scenario is compliant with a given policy."
433
+ )
434
+ scoring_schema = (
435
+ "Does the scenario described in the previous message comply with the "
436
+ "policy? Compliance is determined based on the following guidelines:\n"
437
+ '- "Yes" if the scenario complies with certainty\n'
438
+ '- "No" if the scenario does not comply with certainty\n'
439
+ '- "Ambiguous" if more information is needed\n\n'
440
+ 'Your answer must be either "Yes", "No", or "Ambiguous". '
441
+ 'Return as JSON: {"label": "Yes"/"No"/"Ambiguous"}.'
442
+ )
443
+ policy_block = (
444
+ f"<guardian> {policy_system}\n\n"
445
+ f"### Criteria: Policy: {policy}\n\n"
446
+ f"### Scoring Schema: {scoring_schema}"
447
+ )
448
+ messages = [
449
+ {"role": "user", "content": scenario},
450
+ {"role": "user", "content": policy_block},
451
+ ]
452
+ raw = _generate(messages, adapter="policy-guardrails", max_new_tokens=max_tokens)
453
+ try:
454
+ result = json.loads(raw)
455
+ label = result.get("label", raw)
456
+ return f"**Compliance:** `{label}`\n\n**Raw output:** `{raw}`"
457
+ except json.JSONDecodeError:
458
+ return f"**Raw output:** `{raw}`"
459
+
460
+
461
+ # ---------------------------------------------------------------------------
462
+ # Tab: Context Attribution
463
+ # ---------------------------------------------------------------------------
464
+
465
+
466
+ def run_context_attribution(question, response, documents, max_tokens):
467
+ import re
468
+
469
+ docs = [d.strip() for d in documents.split("\n---\n") if d.strip()]
470
+
471
+ def _split_sentences(text):
472
+ parts = re.split(r"(?<=[.!?])\s+", text.strip())
473
+ return [p for p in parts if p]
474
+
475
+ c_counter = 0
476
+ tagged_doc_parts = []
477
+ for doc in docs:
478
+ parts = []
479
+ for sent in _split_sentences(doc):
480
+ parts.append(f"<c{c_counter}> {sent}")
481
+ c_counter += 1
482
+ tagged_doc_parts.append({"text": " ".join(parts)})
483
+
484
+ response_sents = _split_sentences(response)
485
+ tagged_response = " ".join(f"<r{i}> {s}" for i, s in enumerate(response_sents))
486
+
487
+ instruction = (
488
+ "You provided the last assistant response above based on context, which may "
489
+ "include documents and/or previous conversation turns. Your response is "
490
+ "divided into sentences, numbered in the format <r0> sentence 0 <r1> "
491
+ "sentence 1 ... Sentences in the context are also numbered: <c0> sentence 0 "
492
+ "<c1> sentence 1 ... For each response sentence, please list the context "
493
+ "sentences that were most important for you to generate the response "
494
+ "sentence. Provide your answer in JSON format, as an array of JSON objects, "
495
+ 'where each object has two members: "r" with the response sentence number '
496
+ 'as the value, and "c" with an array of context sentence numbers as the '
497
+ "value. List the context sentences in order from most important to least "
498
+ "important. Ensure that you include an object for each response sentence, "
499
+ "even if the corresponding array of context sentence numbers is empty. "
500
+ "Answer with only the JSON and do not explain.\n"
501
+ )
502
 
503
+ messages = [
504
+ {"role": "user", "content": question},
505
+ {"role": "assistant", "content": tagged_response},
506
+ {"role": "user", "content": instruction},
507
+ ]
508
+ raw = _generate(
509
+ messages, adapter="context-attribution",
510
+ documents=tagged_doc_parts, max_new_tokens=max_tokens
511
+ )
512
+ return f"**Attribution:**\n```json\n{raw}\n```"
513
 
 
 
514
 
515
+ # ---------------------------------------------------------------------------
516
+ # Build the Gradio UI with tabs per adapter
517
+ # ---------------------------------------------------------------------------
518
 
519
 
520
+ with gr.Blocks(title="Granite Switch 4.1 3B Playground") as demo:
521
+ gr.Markdown(
522
+ "# Granite Switch 4.1 3B Playground\n\n"
523
+ "Interactive demo of [ibm-granite/granite-switch-4.1-3b-preview]"
524
+ "(https://huggingface.co/ibm-granite/granite-switch-4.1-3b-preview) "
525
+ "with 12 embedded adapters. Each tab provides the correct prompt "
526
+ "format for its adapter."
527
+ )
528
 
529
+ with gr.Tabs():
530
+ # --- Free Chat ---
531
+ with gr.Tab("Chat (Base Model)"):
532
+ gr.Markdown("Standard chat with the base model. Optionally provide documents for grounded responses.")
533
+ chat_interface = gr.ChatInterface(
534
+ fn=chat_base,
535
+ additional_inputs=[
536
+ gr.Textbox(label="Documents (optional)", lines=4, placeholder="Paste reference documents here..."),
537
+ gr.Slider(16, 512, value=128, step=16, label="Max new tokens"),
538
+ ],
539
+ )
540
+
541
+ # --- Guardian ---
542
+ with gr.Tab("Guardian"):
543
+ gr.Markdown(
544
+ "**guardian-core** β€” Evaluate text for harm, bias, jailbreak, etc.\n\n"
545
+ "Returns `yes` (flagged) or `no` (safe)."
546
+ )
547
+ with gr.Row():
548
+ with gr.Column():
549
+ guardian_text = gr.Textbox(
550
+ label="Text to evaluate",
551
+ lines=3,
552
+ placeholder="Enter the text you want to check for safety...",
553
+ )
554
+ guardian_criteria = gr.Dropdown(
555
+ choices=list(GUARDIAN_CRITERIA_BANK.keys()) + ["Custom"],
556
+ value="harm",
557
+ label="Criteria",
558
+ )
559
+ guardian_custom = gr.Textbox(
560
+ label="Custom criteria (if 'Custom' selected above)",
561
+ lines=2,
562
+ visible=True,
563
+ )
564
+ guardian_tokens = gr.Slider(16, 64, value=20, step=4, label="Max tokens")
565
+ guardian_btn = gr.Button("Evaluate", variant="primary")
566
+ with gr.Column():
567
+ guardian_output = gr.Markdown(label="Result")
568
+ guardian_btn.click(
569
+ run_guardian,
570
+ inputs=[guardian_text, guardian_criteria, guardian_custom, guardian_tokens],
571
+ outputs=guardian_output,
572
+ )
573
+
574
+ # --- Query Rewrite ---
575
+ with gr.Tab("Query Rewrite"):
576
+ gr.Markdown(
577
+ "**query_rewrite** β€” Rewrites messy or verbose queries into clean, search-friendly form."
578
+ )
579
+ with gr.Row():
580
+ with gr.Column():
581
+ qr_query = gr.Textbox(
582
+ label="Original query",
583
+ lines=2,
584
+ placeholder="e.g., what is...mmmm the main city (capital you call it?) of France?",
585
+ )
586
+ qr_tokens = gr.Slider(16, 256, value=64, step=16, label="Max tokens")
587
+ qr_btn = gr.Button("Rewrite", variant="primary")
588
+ with gr.Column():
589
+ qr_output = gr.Markdown(label="Result")
590
+ qr_btn.click(run_query_rewrite, inputs=[qr_query, qr_tokens], outputs=qr_output)
591
+
592
+ # --- Answerability ---
593
+ with gr.Tab("Answerability"):
594
+ gr.Markdown(
595
+ "**answerability** β€” Can the question be answered from the provided documents?\n\n"
596
+ "Separate multiple documents with `---` on its own line."
597
+ )
598
+ with gr.Row():
599
+ with gr.Column():
600
+ ans_question = gr.Textbox(label="Question", lines=2)
601
+ ans_docs = gr.Textbox(
602
+ label="Documents (separated by ---)",
603
+ lines=5,
604
+ placeholder="Document 1 text...\n---\nDocument 2 text...",
605
+ )
606
+ ans_tokens = gr.Slider(16, 128, value=32, step=16, label="Max tokens")
607
+ ans_btn = gr.Button("Check", variant="primary")
608
+ with gr.Column():
609
+ ans_output = gr.Markdown(label="Result")
610
+ ans_btn.click(
611
+ run_answerability,
612
+ inputs=[ans_question, ans_docs, ans_tokens],
613
+ outputs=ans_output,
614
+ )
615
+
616
+ # --- Citations ---
617
+ with gr.Tab("Citations"):
618
+ gr.Markdown(
619
+ "**citations** β€” Find which document passages support a given answer.\n\n"
620
+ "Separate multiple documents with `---`."
621
+ )
622
+ with gr.Row():
623
+ with gr.Column():
624
+ cit_question = gr.Textbox(label="Question", lines=2)
625
+ cit_answer = gr.Textbox(label="Answer to attribute", lines=3)
626
+ cit_docs = gr.Textbox(
627
+ label="Documents (separated by ---)", lines=5,
628
+ )
629
+ cit_tokens = gr.Slider(16, 256, value=128, step=16, label="Max tokens")
630
+ cit_btn = gr.Button("Find Citations", variant="primary")
631
+ with gr.Column():
632
+ cit_output = gr.Markdown(label="Result")
633
+ cit_btn.click(
634
+ run_citations,
635
+ inputs=[cit_question, cit_answer, cit_docs, cit_tokens],
636
+ outputs=cit_output,
637
+ )
638
+
639
+ # --- Hallucination Detection ---
640
+ with gr.Tab("Hallucination Detection"):
641
+ gr.Markdown(
642
+ "**hallucination_detection** β€” Detect hallucinated content in a response "
643
+ "relative to source documents.\n\nSeparate documents with `---`."
644
+ )
645
+ with gr.Row():
646
+ with gr.Column():
647
+ hall_question = gr.Textbox(label="Question", lines=2)
648
+ hall_answer = gr.Textbox(label="Response to check", lines=3)
649
+ hall_docs = gr.Textbox(label="Source documents (separated by ---)", lines=5)
650
+ hall_tokens = gr.Slider(16, 256, value=64, step=16, label="Max tokens")
651
+ hall_btn = gr.Button("Detect", variant="primary")
652
+ with gr.Column():
653
+ hall_output = gr.Markdown(label="Result")
654
+ hall_btn.click(
655
+ run_hallucination_detection,
656
+ inputs=[hall_question, hall_answer, hall_docs, hall_tokens],
657
+ outputs=hall_output,
658
+ )
659
+
660
+ # --- Uncertainty ---
661
+ with gr.Tab("Uncertainty"):
662
+ gr.Markdown(
663
+ "**uncertainty** β€” Returns a calibrated confidence digit (0-9) for the "
664
+ "last assistant response.\n\n"
665
+ "Digit maps to probability: `0.1 * digit + 0.05` (5% to 95%)."
666
+ )
667
+ with gr.Row():
668
+ with gr.Column():
669
+ unc_text = gr.Textbox(
670
+ label="Assistant response to evaluate certainty of",
671
+ lines=4,
672
+ placeholder="Paste the response you want to gauge confidence for...",
673
+ )
674
+ unc_tokens = gr.Slider(16, 32, value=20, step=4, label="Max tokens")
675
+ unc_btn = gr.Button("Check Certainty", variant="primary")
676
+ with gr.Column():
677
+ unc_output = gr.Markdown(label="Result")
678
+ unc_btn.click(run_uncertainty, inputs=[unc_text, unc_tokens], outputs=unc_output)
679
+
680
+ # --- Requirement Check ---
681
+ with gr.Tab("Requirement Check"):
682
+ gr.Markdown(
683
+ "**requirement-check** β€” Does the assistant's response satisfy "
684
+ "stated requirements?\n\nReturns `yes` or `no`."
685
+ )
686
+ with gr.Row():
687
+ with gr.Column():
688
+ req_question = gr.Textbox(label="User question", lines=2)
689
+ req_response = gr.Textbox(label="Assistant response", lines=4)
690
+ req_requirements = gr.Textbox(
691
+ label="Requirements",
692
+ lines=3,
693
+ placeholder="e.g., Must be formal tone. Under 100 words. Must cite sources.",
694
+ )
695
+ req_tokens = gr.Slider(16, 32, value=20, step=4, label="Max tokens")
696
+ req_btn = gr.Button("Check", variant="primary")
697
+ with gr.Column():
698
+ req_output = gr.Markdown(label="Result")
699
+ req_btn.click(
700
+ run_requirement_check,
701
+ inputs=[req_question, req_response, req_requirements, req_tokens],
702
+ outputs=req_output,
703
+ )
704
+
705
+ # --- Factuality Detection ---
706
+ with gr.Tab("Factuality Detection"):
707
+ gr.Markdown(
708
+ "**factuality-detection** β€” Check if a response contains factual errors "
709
+ "vs source documents.\n\nSeparate documents with `---`."
710
+ )
711
+ with gr.Row():
712
+ with gr.Column():
713
+ fd_response = gr.Textbox(label="Response to check", lines=4)
714
+ fd_docs = gr.Textbox(label="Source documents (separated by ---)", lines=5)
715
+ fd_tokens = gr.Slider(16, 32, value=20, step=4, label="Max tokens")
716
+ fd_btn = gr.Button("Detect", variant="primary")
717
+ with gr.Column():
718
+ fd_output = gr.Markdown(label="Result")
719
+ fd_btn.click(
720
+ run_factuality_detection,
721
+ inputs=[fd_response, fd_docs, fd_tokens],
722
+ outputs=fd_output,
723
+ )
724
+
725
+ # --- Factuality Correction ---
726
+ with gr.Tab("Factuality Correction"):
727
+ gr.Markdown(
728
+ "**factuality-correction** β€” Correct factual errors in a response "
729
+ "using source documents.\n\nSeparate documents with `---`."
730
+ )
731
+ with gr.Row():
732
+ with gr.Column():
733
+ fc_response = gr.Textbox(label="Response to correct", lines=4)
734
+ fc_docs = gr.Textbox(label="Source documents (separated by ---)", lines=5)
735
+ fc_tokens = gr.Slider(16, 512, value=256, step=16, label="Max tokens")
736
+ fc_btn = gr.Button("Correct", variant="primary")
737
+ with gr.Column():
738
+ fc_output = gr.Markdown(label="Result")
739
+ fc_btn.click(
740
+ run_factuality_correction,
741
+ inputs=[fc_response, fc_docs, fc_tokens],
742
+ outputs=fc_output,
743
+ )
744
+
745
+ # --- Policy Guardrails ---
746
+ with gr.Tab("Policy Guardrails"):
747
+ gr.Markdown(
748
+ "**policy-guardrails** β€” Check if a scenario complies with a policy.\n\n"
749
+ "Returns `Yes`, `No`, or `Ambiguous`."
750
+ )
751
+ with gr.Row():
752
+ with gr.Column():
753
+ pol_scenario = gr.Textbox(
754
+ label="Scenario (text to evaluate)",
755
+ lines=4,
756
+ placeholder="The assistant response or action to judge...",
757
+ )
758
+ pol_policy = gr.Textbox(
759
+ label="Policy",
760
+ lines=3,
761
+ placeholder="e.g., Responses must not provide investment advice.",
762
+ )
763
+ pol_tokens = gr.Slider(16, 32, value=20, step=4, label="Max tokens")
764
+ pol_btn = gr.Button("Evaluate", variant="primary")
765
+ with gr.Column():
766
+ pol_output = gr.Markdown(label="Result")
767
+ pol_btn.click(
768
+ run_policy_guardrails,
769
+ inputs=[pol_scenario, pol_policy, pol_tokens],
770
+ outputs=pol_output,
771
+ )
772
+
773
+ # --- Context Attribution ---
774
+ with gr.Tab("Context Attribution"):
775
+ gr.Markdown(
776
+ "**context-attribution** β€” Which context sentences supported each "
777
+ "sentence of the response?\n\nSeparate documents with `---`."
778
+ )
779
+ with gr.Row():
780
+ with gr.Column():
781
+ ca_question = gr.Textbox(label="Question", lines=2)
782
+ ca_response = gr.Textbox(label="Response to attribute", lines=4)
783
+ ca_docs = gr.Textbox(label="Context documents (separated by ---)", lines=5)
784
+ ca_tokens = gr.Slider(16, 512, value=256, step=16, label="Max tokens")
785
+ ca_btn = gr.Button("Attribute", variant="primary")
786
+ with gr.Column():
787
+ ca_output = gr.Markdown(label="Result")
788
+ ca_btn.click(
789
+ run_context_attribution,
790
+ inputs=[ca_question, ca_response, ca_docs, ca_tokens],
791
+ outputs=ca_output,
792
+ )
793
+
794
+ if __name__ == "__main__":
795
+ demo.launch()
796
 
 
 
 
 
requirements.txt CHANGED
@@ -2,3 +2,9 @@ fastapi
2
  uvicorn
3
  python-multipart
4
  jinja2
 
 
 
 
 
 
 
2
  uvicorn
3
  python-multipart
4
  jinja2
5
+
6
+ torch
7
+ transformers
8
+ accelerate
9
+ granite-switch[hf] @ git+https://github.com/generative-computing/granite-switch.git
10
+ gradio