rasa2 commited on
Commit
a0cc9e0
·
1 Parent(s): 7d5e8cb

Add model, GPU and prompt options

Browse files
Files changed (1) hide show
  1. app.py +364 -46
app.py CHANGED
@@ -9,15 +9,60 @@ from fastapi.responses import HTMLResponse, RedirectResponse
9
  from pydantic import BaseModel
10
 
11
 
 
 
 
 
12
  BROKER_TOKEN = os.environ.get("BROKER_TOKEN")
13
  UI_TOKEN = os.environ.get("UI_TOKEN")
14
 
15
  if not BROKER_TOKEN:
16
- raise RuntimeError("Missing BROKER_TOKEN")
 
 
 
17
 
18
  if not UI_TOKEN:
19
- raise RuntimeError("Missing UI_TOKEN")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
 
 
 
21
 
22
  class JobStatus(str, Enum):
23
  queued = "queued"
@@ -32,32 +77,78 @@ class Job(BaseModel):
32
  status: JobStatus = JobStatus.queued
33
  result: Optional[str] = None
34
 
 
 
 
 
 
 
35
 
36
  app = FastAPI(title="Fury Broker")
37
 
 
38
  jobs: dict[str, Job] = {}
39
 
40
 
41
- ALLOWED_COMMANDS = {
42
- "hostname": "hostname",
43
- "whoami": "whoami",
44
- "pwd": "pwd",
45
- "disk": "df -h",
46
- "date": "date",
47
- "list_home": "ls -lah ~ | head -50",
48
- }
49
-
50
 
51
  def verify_broker_token(x_broker_token: Optional[str]) -> None:
 
 
 
 
 
 
52
  if x_broker_token != BROKER_TOKEN:
53
  raise HTTPException(status_code=401, detail="Unauthorized")
54
 
55
 
56
  def verify_ui_token(ui_token: Optional[str]) -> None:
 
 
 
 
 
57
  if ui_token != UI_TOKEN:
58
  raise HTTPException(status_code=401, detail="Invalid UI token")
59
 
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  @app.get("/health")
62
  def health():
63
  return {
@@ -67,25 +158,77 @@ def health():
67
  }
68
 
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  @app.get("/", response_class=HTMLResponse)
71
  def home():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  rows = ""
73
 
74
  for job in reversed(list(jobs.values())):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  rows += f"""
76
  <tr>
77
  <td><code>{escape(job.id)}</code></td>
78
  <td>{escape(job.command)}</td>
79
  <td>{escape(job.status.value)}</td>
80
- <td><pre>{escape(job.result or "")}</pre></td>
 
81
  </tr>
82
  """
83
 
84
- options = "\n".join(
85
- f'<option value="{escape(name)}">{escape(name)} → {escape(cmd)}</option>'
86
- for name, cmd in ALLOWED_COMMANDS.items()
87
- )
88
-
89
  return f"""
90
  <!doctype html>
91
  <html>
@@ -95,9 +238,16 @@ def home():
95
  body {{
96
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
97
  margin: 40px;
98
- max-width: 1200px;
99
  line-height: 1.45;
100
  }}
 
 
 
 
 
 
 
101
  form {{
102
  margin: 1.5rem 0;
103
  padding: 1rem;
@@ -105,10 +255,14 @@ def home():
105
  border-radius: 8px;
106
  background: #fafafa;
107
  }}
108
- input, select, button {{
109
  padding: 6px;
110
  margin: 4px;
111
  }}
 
 
 
 
112
  table {{
113
  border-collapse: collapse;
114
  width: 100%;
@@ -125,23 +279,34 @@ def home():
125
  }}
126
  pre {{
127
  white-space: pre-wrap;
128
- max-width: 800px;
129
- max-height: 400px;
130
  overflow: auto;
131
  margin: 0;
132
  }}
 
 
 
 
 
 
 
133
  </style>
134
  </head>
135
  <body>
136
  <h1>Fury Broker</h1>
 
 
 
137
 
138
- <p>
139
- Submit an approved command from Hugging Face Spaces.
140
- The worker running on Fury will pick it up, execute it on Fury,
141
- and return the result here.
142
- </p>
 
 
143
 
144
- <form method="post" action="/submit">
145
  <div>
146
  <label><strong>UI token:</strong></label>
147
  <input type="password" name="ui_token" placeholder="Enter UI_TOKEN" required>
@@ -150,10 +315,53 @@ def home():
150
  <div>
151
  <label><strong>Command:</strong></label>
152
  <select name="command">
153
- {options}
154
  </select>
155
- <button type="submit">Submit job</button>
156
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  </form>
158
 
159
  <p>
@@ -167,6 +375,7 @@ def home():
167
  <th>ID</th>
168
  <th>Command</th>
169
  <th>Status</th>
 
170
  <th>Result</th>
171
  </tr>
172
  {rows}
@@ -176,15 +385,17 @@ def home():
176
  """
177
 
178
 
179
- @app.post("/submit")
180
- def submit_from_ui(
 
 
 
 
181
  command: str = Form(...),
182
  ui_token: str = Form(...),
183
  ):
184
  verify_ui_token(ui_token)
185
-
186
- if command not in ALLOWED_COMMANDS:
187
- raise HTTPException(status_code=400, detail="Command is not allowed")
188
 
189
  job_id = str(uuid.uuid4())
190
 
@@ -197,15 +408,49 @@ def submit_from_ui(
197
  return RedirectResponse("/", status_code=303)
198
 
199
 
200
- @app.post("/api/jobs")
201
- def submit_job(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  command: str = Form(...),
203
  x_broker_token: Optional[str] = Header(default=None),
204
  ):
205
  verify_broker_token(x_broker_token)
206
-
207
- if command not in ALLOWED_COMMANDS:
208
- raise HTTPException(status_code=400, detail="Command is not allowed")
209
 
210
  job_id = str(uuid.uuid4())
211
 
@@ -216,10 +461,67 @@ def submit_job(
216
  )
217
 
218
  jobs[job_id] = job
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
 
 
 
 
 
 
 
 
 
220
  return job
221
 
222
 
 
 
 
 
223
  @app.get("/api/next-job")
224
  def get_next_job(x_broker_token: Optional[str] = Header(default=None)):
225
  verify_broker_token(x_broker_token)
@@ -231,7 +533,10 @@ def get_next_job(x_broker_token: Optional[str] = Header(default=None)):
231
  return {
232
  "id": job.id,
233
  "command": job.command,
234
- "shell_command": ALLOWED_COMMANDS[job.command],
 
 
 
235
  }
236
 
237
  return {"id": None}
@@ -262,13 +567,26 @@ def list_jobs(x_broker_token: Optional[str] = Header(default=None)):
262
  return {"jobs": list(jobs.values())}
263
 
264
 
265
- @app.get("/api/commands")
266
- def list_commands(x_broker_token: Optional[str] = Header(default=None)):
 
 
 
267
  verify_broker_token(x_broker_token)
268
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  return {
270
- "commands": [
271
- {"name": name, "shell_command": cmd}
272
- for name, cmd in ALLOWED_COMMANDS.items()
273
- ]
274
  }
 
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 exposed in the UI
34
+ # =============================================================================
35
+
36
+ MODEL_OPTIONS = {
37
+ "granite-4.0-micro": "ibm-granite/granite-4.0-micro",
38
+ "granite-4.1-8b": "ibm-granite/granite-4.1-8b",
39
+ "granite-4.1-30b": "ibm-granite/granite-4.1-30b",
40
+ "qwen2.5-coder-32b": "Qwen/Qwen2.5-Coder-32B-Instruct",
41
+ }
42
+
43
+ # Replace these with your real second-parameter options.
44
+ # This is intentionally a fixed allowlist.
45
+ RUN_MODE_OPTIONS = {
46
+ "scan": "scan",
47
+ "summarize": "summarize",
48
+ "full": "full",
49
+ }
50
+
51
+ # Simple predefined operational commands.
52
+ # These do not accept user free text.
53
+ BASIC_COMMANDS = {
54
+ "hostname": "Run hostname",
55
+ "whoami": "Run whoami",
56
+ "pwd": "Show current directory",
57
+ "disk": "Show disk usage",
58
+ "date": "Show current date",
59
+ "list_home": "List home directory",
60
+ }
61
+
62
 
63
+ # =============================================================================
64
+ # Data model
65
+ # =============================================================================
66
 
67
  class JobStatus(str, Enum):
68
  queued = "queued"
 
77
  status: JobStatus = JobStatus.queued
78
  result: Optional[str] = None
79
 
80
+ # Parameters for run_paper_reader
81
+ model: Optional[str] = None
82
+ run_mode: Optional[str] = None
83
+ gpus: Optional[int] = None
84
+ user_text: Optional[str] = None
85
+
86
 
87
  app = FastAPI(title="Fury Broker")
88
 
89
+ # In-memory storage. Jobs disappear if the Space restarts.
90
  jobs: dict[str, Job] = {}
91
 
92
 
93
+ # =============================================================================
94
+ # Security helpers
95
+ # =============================================================================
 
 
 
 
 
 
96
 
97
  def verify_broker_token(x_broker_token: Optional[str]) -> None:
98
+ """
99
+ Used by the Fury worker.
100
+
101
+ The worker sends:
102
+ X-Broker-Token: BROKER_TOKEN
103
+ """
104
  if x_broker_token != BROKER_TOKEN:
105
  raise HTTPException(status_code=401, detail="Unauthorized")
106
 
107
 
108
  def verify_ui_token(ui_token: Optional[str]) -> None:
109
+ """
110
+ Used by the browser UI.
111
+
112
+ The browser form sends the UI token as a form field.
113
+ """
114
  if ui_token != UI_TOKEN:
115
  raise HTTPException(status_code=401, detail="Invalid UI token")
116
 
117
 
118
+ def validate_basic_command(command: str) -> None:
119
+ if command not in BASIC_COMMANDS:
120
+ raise HTTPException(status_code=400, detail=f"Command is not allowed: {command}")
121
+
122
+
123
+ def validate_paper_reader_args(
124
+ model: str,
125
+ run_mode: str,
126
+ gpus: int,
127
+ user_text: str,
128
+ ) -> None:
129
+ if model not in MODEL_OPTIONS:
130
+ raise HTTPException(status_code=400, detail=f"Model is not allowed: {model}")
131
+
132
+ if run_mode not in RUN_MODE_OPTIONS:
133
+ raise HTTPException(status_code=400, detail=f"Run mode is not allowed: {run_mode}")
134
+
135
+ if gpus < 1 or gpus > 16:
136
+ raise HTTPException(status_code=400, detail="GPUs must be between 1 and 16")
137
+
138
+ if user_text is None:
139
+ raise HTTPException(status_code=400, detail="User text is required")
140
+
141
+ if len(user_text.strip()) == 0:
142
+ raise HTTPException(status_code=400, detail="User text cannot be empty")
143
+
144
+ if len(user_text) > 10_000:
145
+ raise HTTPException(status_code=400, detail="User text is too long; max 10,000 characters")
146
+
147
+
148
+ # =============================================================================
149
+ # Health and API inspection
150
+ # =============================================================================
151
+
152
  @app.get("/health")
153
  def health():
154
  return {
 
158
  }
159
 
160
 
161
+ @app.get("/api/commands")
162
+ def list_commands(x_broker_token: Optional[str] = Header(default=None)):
163
+ verify_broker_token(x_broker_token)
164
+
165
+ return {
166
+ "basic_commands": BASIC_COMMANDS,
167
+ "model_options": MODEL_OPTIONS,
168
+ "run_mode_options": RUN_MODE_OPTIONS,
169
+ "gpu_range": [1, 16],
170
+ }
171
+
172
+
173
+ # =============================================================================
174
+ # Browser UI
175
+ # =============================================================================
176
+
177
  @app.get("/", response_class=HTMLResponse)
178
  def home():
179
+ basic_command_options_html = "\n".join(
180
+ f'<option value="{escape(name)}">{escape(name)} — {escape(label)}</option>'
181
+ for name, label in BASIC_COMMANDS.items()
182
+ )
183
+
184
+ model_options_html = "\n".join(
185
+ f'<option value="{escape(key)}">{escape(value)}</option>'
186
+ for key, value in MODEL_OPTIONS.items()
187
+ )
188
+
189
+ run_mode_options_html = "\n".join(
190
+ f'<option value="{escape(key)}">{escape(value)}</option>'
191
+ for key, value in RUN_MODE_OPTIONS.items()
192
+ )
193
+
194
+ gpu_options_html = "\n".join(
195
+ f'<option value="{i}">{i}</option>'
196
+ for i in range(1, 17)
197
+ )
198
+
199
  rows = ""
200
 
201
  for job in reversed(list(jobs.values())):
202
+ details = []
203
+
204
+ if job.model:
205
+ details.append(f"model={job.model}")
206
+
207
+ if job.run_mode:
208
+ details.append(f"run_mode={job.run_mode}")
209
+
210
+ if job.gpus:
211
+ details.append(f"gpus={job.gpus}")
212
+
213
+ if job.user_text:
214
+ preview = job.user_text[:500]
215
+ if len(job.user_text) > 500:
216
+ preview += "..."
217
+ details.append(f"user_text={preview}")
218
+
219
+ safe_details = escape("\n".join(details))
220
+ safe_result = escape(job.result or "")
221
+
222
  rows += f"""
223
  <tr>
224
  <td><code>{escape(job.id)}</code></td>
225
  <td>{escape(job.command)}</td>
226
  <td>{escape(job.status.value)}</td>
227
+ <td><pre>{safe_details}</pre></td>
228
+ <td><pre>{safe_result}</pre></td>
229
  </tr>
230
  """
231
 
 
 
 
 
 
232
  return f"""
233
  <!doctype html>
234
  <html>
 
238
  body {{
239
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
240
  margin: 40px;
241
+ max-width: 1300px;
242
  line-height: 1.45;
243
  }}
244
+ h1 {{
245
+ margin-bottom: 0.2rem;
246
+ }}
247
+ .subtitle {{
248
+ color: #555;
249
+ margin-bottom: 2rem;
250
+ }}
251
  form {{
252
  margin: 1.5rem 0;
253
  padding: 1rem;
 
255
  border-radius: 8px;
256
  background: #fafafa;
257
  }}
258
+ input, select, textarea, button {{
259
  padding: 6px;
260
  margin: 4px;
261
  }}
262
+ textarea {{
263
+ width: 95%;
264
+ font-family: monospace;
265
+ }}
266
  table {{
267
  border-collapse: collapse;
268
  width: 100%;
 
279
  }}
280
  pre {{
281
  white-space: pre-wrap;
282
+ max-width: 600px;
283
+ max-height: 500px;
284
  overflow: auto;
285
  margin: 0;
286
  }}
287
+ .warning {{
288
+ color: #8a4b00;
289
+ background: #fff4dd;
290
+ border: 1px solid #f0c36d;
291
+ padding: 0.8rem;
292
+ border-radius: 6px;
293
+ }}
294
  </style>
295
  </head>
296
  <body>
297
  <h1>Fury Broker</h1>
298
+ <div class="subtitle">
299
+ Submit approved jobs from Hugging Face Spaces to the worker running on Fury.
300
+ </div>
301
 
302
+ <div class="warning">
303
+ This UI does not allow arbitrary shell commands. It only submits predefined
304
+ command names and validated parameters. The Fury worker validates again locally.
305
+ </div>
306
+
307
+ <form method="post" action="/submit-basic">
308
+ <h2>Basic Fury Command</h2>
309
 
 
310
  <div>
311
  <label><strong>UI token:</strong></label>
312
  <input type="password" name="ui_token" placeholder="Enter UI_TOKEN" required>
 
315
  <div>
316
  <label><strong>Command:</strong></label>
317
  <select name="command">
318
+ {basic_command_options_html}
319
  </select>
 
320
  </div>
321
+
322
+ <button type="submit">Submit basic job</button>
323
+ </form>
324
+
325
+ <form method="post" action="/submit-paper-reader">
326
+ <h2>Run Paper Reader on Fury</h2>
327
+
328
+ <div>
329
+ <label><strong>UI token:</strong></label>
330
+ <input type="password" name="ui_token" placeholder="Enter UI_TOKEN" required>
331
+ </div>
332
+
333
+ <div>
334
+ <label><strong>Model:</strong></label>
335
+ <select name="model">
336
+ {model_options_html}
337
+ </select>
338
+ </div>
339
+
340
+ <div>
341
+ <label><strong>Run mode:</strong></label>
342
+ <select name="run_mode">
343
+ {run_mode_options_html}
344
+ </select>
345
+ </div>
346
+
347
+ <div>
348
+ <label><strong>Number of GPUs:</strong></label>
349
+ <select name="gpus">
350
+ {gpu_options_html}
351
+ </select>
352
+ </div>
353
+
354
+ <div>
355
+ <label><strong>Free text to send to the LLM:</strong></label><br>
356
+ <textarea
357
+ name="user_text"
358
+ rows="10"
359
+ placeholder="Enter the text/question/instructions to send to the model..."
360
+ required
361
+ ></textarea>
362
+ </div>
363
+
364
+ <button type="submit">Submit paper reader job</button>
365
  </form>
366
 
367
  <p>
 
375
  <th>ID</th>
376
  <th>Command</th>
377
  <th>Status</th>
378
+ <th>Parameters</th>
379
  <th>Result</th>
380
  </tr>
381
  {rows}
 
385
  """
386
 
387
 
388
+ # =============================================================================
389
+ # Browser submit endpoints
390
+ # =============================================================================
391
+
392
+ @app.post("/submit-basic")
393
+ def submit_basic_from_ui(
394
  command: str = Form(...),
395
  ui_token: str = Form(...),
396
  ):
397
  verify_ui_token(ui_token)
398
+ validate_basic_command(command)
 
 
399
 
400
  job_id = str(uuid.uuid4())
401
 
 
408
  return RedirectResponse("/", status_code=303)
409
 
410
 
411
+ @app.post("/submit-paper-reader")
412
+ def submit_paper_reader_from_ui(
413
+ model: str = Form(...),
414
+ run_mode: str = Form(...),
415
+ gpus: int = Form(...),
416
+ user_text: str = Form(...),
417
+ ui_token: str = Form(...),
418
+ ):
419
+ verify_ui_token(ui_token)
420
+
421
+ validate_paper_reader_args(
422
+ model=model,
423
+ run_mode=run_mode,
424
+ gpus=gpus,
425
+ user_text=user_text,
426
+ )
427
+
428
+ job_id = str(uuid.uuid4())
429
+
430
+ jobs[job_id] = Job(
431
+ id=job_id,
432
+ command="run_paper_reader",
433
+ model=model,
434
+ run_mode=run_mode,
435
+ gpus=gpus,
436
+ user_text=user_text,
437
+ status=JobStatus.queued,
438
+ )
439
+
440
+ return RedirectResponse("/", status_code=303)
441
+
442
+
443
+ # =============================================================================
444
+ # API submit endpoints
445
+ # =============================================================================
446
+
447
+ @app.post("/api/jobs/basic")
448
+ def submit_basic_job_api(
449
  command: str = Form(...),
450
  x_broker_token: Optional[str] = Header(default=None),
451
  ):
452
  verify_broker_token(x_broker_token)
453
+ validate_basic_command(command)
 
 
454
 
455
  job_id = str(uuid.uuid4())
456
 
 
461
  )
462
 
463
  jobs[job_id] = job
464
+ return job
465
+
466
+
467
+ @app.post("/api/jobs/paper-reader")
468
+ def submit_paper_reader_job_api(
469
+ model: str = Form(...),
470
+ run_mode: str = Form(...),
471
+ gpus: int = Form(...),
472
+ user_text: str = Form(...),
473
+ x_broker_token: Optional[str] = Header(default=None),
474
+ ):
475
+ verify_broker_token(x_broker_token)
476
+
477
+ validate_paper_reader_args(
478
+ model=model,
479
+ run_mode=run_mode,
480
+ gpus=gpus,
481
+ user_text=user_text,
482
+ )
483
+
484
+ job_id = str(uuid.uuid4())
485
+
486
+ job = Job(
487
+ id=job_id,
488
+ command="run_paper_reader",
489
+ model=model,
490
+ run_mode=run_mode,
491
+ gpus=gpus,
492
+ user_text=user_text,
493
+ status=JobStatus.queued,
494
+ )
495
+
496
+ jobs[job_id] = job
497
+ return job
498
+
499
+
500
+ # Backward-compatible endpoint for your previous add_job.sh.
501
+ @app.post("/api/jobs")
502
+ def submit_job_legacy_api(
503
+ command: str = Form(...),
504
+ x_broker_token: Optional[str] = Header(default=None),
505
+ ):
506
+ verify_broker_token(x_broker_token)
507
+ validate_basic_command(command)
508
 
509
+ job_id = str(uuid.uuid4())
510
+
511
+ job = Job(
512
+ id=job_id,
513
+ command=command,
514
+ status=JobStatus.queued,
515
+ )
516
+
517
+ jobs[job_id] = job
518
  return job
519
 
520
 
521
+ # =============================================================================
522
+ # Worker polling and result posting
523
+ # =============================================================================
524
+
525
  @app.get("/api/next-job")
526
  def get_next_job(x_broker_token: Optional[str] = Header(default=None)):
527
  verify_broker_token(x_broker_token)
 
533
  return {
534
  "id": job.id,
535
  "command": job.command,
536
+ "model": job.model,
537
+ "run_mode": job.run_mode,
538
+ "gpus": job.gpus,
539
+ "user_text": job.user_text,
540
  }
541
 
542
  return {"id": None}
 
567
  return {"jobs": list(jobs.values())}
568
 
569
 
570
+ @app.get("/api/jobs/{job_id}")
571
+ def get_job(
572
+ job_id: str,
573
+ x_broker_token: Optional[str] = Header(default=None),
574
+ ):
575
  verify_broker_token(x_broker_token)
576
 
577
+ if job_id not in jobs:
578
+ raise HTTPException(status_code=404, detail="Job not found")
579
+
580
+ return jobs[job_id]
581
+
582
+
583
+ @app.post("/api/clear")
584
+ def clear_jobs(x_broker_token: Optional[str] = Header(default=None)):
585
+ verify_broker_token(x_broker_token)
586
+
587
+ jobs.clear()
588
+
589
  return {
590
+ "status": "cleared",
591
+ "jobs_count": len(jobs),
 
 
592
  }