igardner commited on
Commit
6092030
·
1 Parent(s): 799874f

feat: add Gradio smoke-test dashboard

Browse files

- replace the default template with a Gradio Blocks app
- show live HF_TOKEN status (unset, invalid, or authenticated account and plan)
- add a model-count slider (default 30) and a primary Run button
- stream per-model progress while the runner works
- render a summary with passed count, failures, fallback recoveries, and duration
- render a results table with literal True/False per model, attempt used, latency, and error
- load previous runs from /data/hf-inference-smoke via a history dropdown
- offer the current run JSON as a download
- populate the latest run and token banner on page load
- move the theme into launch() for Gradio 6 compatibility
- document discovery, routing, pass criteria, budget, and storage in-app

Files changed (1) hide show
  1. app.py +252 -4
app.py CHANGED
@@ -1,7 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
1
+ """StockAnalysis - HF Inference Providers smoke-test dashboard.
2
+
3
+ The app resolves the most-trending conversational models on the Hub that have
4
+ at least one live inference provider, sends one tiny chat completion to each
5
+ via the HF router (cheapest routing, one fastest fallback), and reports a
6
+ literal True/False per model.
7
+
8
+ Run artifacts are written to /data/hf-inference-smoke so they survive restarts.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
  import gradio as gr
19
 
20
+ import smoke
21
+
22
+ TABLE_HEADERS = ["Rank", "Model", "Passed", "Attempt", "Latency (ms)", "Error"]
23
+ TABLE_TYPES = ["number", "str", "bool", "str", "number", "str"]
24
+
25
+ HOW_IT_WORKS = """
26
+ ### How this works
27
+
28
+ 1. **Discovery** - queries the Hub for `conversational` models sorted by
29
+ `trendingScore`, scanning past the requested count so models without a live
30
+ inference provider are skipped.
31
+ 2. **Routing** - each model is called through the HF router with `:cheapest`.
32
+ If that attempt fails, it is retried once with `:fastest`.
33
+ 3. **Pass criteria** - a model is `True` only when the router returns HTTP 200
34
+ with a non-empty `choices` array.
35
+ 4. **Budget** - each attempt asks for at most 8 output tokens, so a 30-model run
36
+ costs well under a cent.
37
+ 5. **Storage** - every run is saved to `/data/hf-inference-smoke/runs/` and
38
+ mirrored to `latest.json`.
39
+
40
+ Results are written to persistent storage, never to the Space repository.
41
+ """
42
+
43
+
44
+ def _token_banner() -> str:
45
+ """Return a short markdown banner describing the HF token state."""
46
+ token = os.getenv("HF_TOKEN")
47
+ if not token:
48
+ return (
49
+ "**Token status:** `HF_TOKEN` is **not set**. "
50
+ "Add it as a Space secret or environment variable to run tests."
51
+ )
52
+
53
+ status = smoke.get_token_status(token)
54
+ if not status["ok"]:
55
+ return f"**Token status:** invalid - {status['error']}"
56
+
57
+ plan = status["plan"] or "unknown"
58
+ return f"**Token status:** authenticated as **{status['name']}** (plan: `{plan}`)"
59
+
60
+
61
+ def _summary(run: dict[str, Any] | None) -> str:
62
+ if not run:
63
+ return "No runs yet. Press **Run smoke test** to create one."
64
+
65
+ total = run.get("model_limit") or len(run.get("results", []))
66
+ passed = run.get("passed", 0)
67
+ failed = run.get("failed", 0)
68
+ fallbacks = run.get("fallbacks", 0)
69
+ duration = run.get("duration_seconds", 0)
70
+ return (
71
+ f"### Run `{run.get('run_id')}`\n\n"
72
+ f"| Metric | Value |\n"
73
+ f"|---|---|\n"
74
+ f"| **Passed** | **{passed} / {total}** |\n"
75
+ f"| Failed | {failed} |\n"
76
+ f"| Recovered on `:fastest` | {fallbacks} |\n"
77
+ f"| Duration | {duration}s |\n"
78
+ f"| Started | {run.get('started_at')} |\n"
79
+ f"| Finished | {run.get('finished_at')} |\n"
80
+ )
81
+
82
+
83
+ def _rows(run: dict[str, Any] | None) -> list[list[Any]]:
84
+ if not run:
85
+ return []
86
+ return [
87
+ [
88
+ item.get("rank"),
89
+ item.get("model"),
90
+ bool(item.get("ok")),
91
+ item.get("attempt"),
92
+ item.get("latency_ms"),
93
+ item.get("error") or "",
94
+ ]
95
+ for item in run.get("results", [])
96
+ ]
97
+
98
+
99
+ def _run_options() -> list[tuple[str, str]]:
100
+ options: list[tuple[str, str]] = []
101
+ for meta in smoke.list_runs():
102
+ label = (
103
+ f"{meta.get('run_id')} - {meta.get('passed')}/"
104
+ f"{meta.get('model_limit')} passed"
105
+ )
106
+ options.append((label, meta.get("run_id")))
107
+ return options
108
+
109
+
110
+ def _download_path(run: dict[str, Any] | None) -> str | None:
111
+ if not run:
112
+ return None
113
+ run_id = run.get("run_id")
114
+ if not run_id:
115
+ return None
116
+ path = smoke.RUNS_DIR / f"{run_id}.json"
117
+ return str(path) if path.exists() else None
118
+
119
+
120
+ def _view(
121
+ run: dict[str, Any] | None,
122
+ ) -> tuple[str, list[list[Any]], str | None]:
123
+ return _summary(run), _rows(run), _download_path(run)
124
+
125
+
126
+ def initial_view() -> tuple[str, list[list[Any]], str | None, Any, str]:
127
+ run = smoke.load_latest_run()
128
+ summary, rows, download = _view(run)
129
+ choices = _run_options()
130
+ selected = run.get("run_id") if run else None
131
+ return summary, rows, download, gr.update(choices=choices, value=selected), _token_banner()
132
+
133
+
134
+ def run_smoke_test(
135
+ model_limit: int,
136
+ progress: gr.Progress = gr.Progress(),
137
+ ) -> tuple[str, list[list[Any]], str | None, Any, str]:
138
+ token = os.getenv("HF_TOKEN")
139
+ if not token:
140
+ raise gr.Error(
141
+ "HF_TOKEN is not set. Add it as a Space secret or environment variable, "
142
+ "then restart the Space."
143
+ )
144
+
145
+ status = smoke.get_token_status(token)
146
+ if not status["ok"]:
147
+ raise gr.Error(f"HF_TOKEN was rejected: {status['error']}")
148
+
149
+ progress(0.0, desc="Fetching trending models...")
150
+ try:
151
+ snapshot = smoke.fetch_trending_models(limit=int(model_limit))
152
+ except Exception as exc: # noqa: BLE001 - surface discovery failures in the UI
153
+ raise gr.Error(f"Model discovery failed: {exc}") from exc
154
+
155
+ models = snapshot["models"]
156
+ total = len(models)
157
+
158
+ def on_result(done: int, count: int, result: dict[str, Any]) -> None:
159
+ state = "PASS" if result["ok"] else "FAIL"
160
+ progress(
161
+ done / count,
162
+ desc=f"{done}/{count} - {state} {result['model']} ({result['attempt']})",
163
+ )
164
+
165
+ progress(0.0, desc=f"Testing {total} models...")
166
+ try:
167
+ run = smoke.run_smoke(models, token=token, progress=on_result)
168
+ except Exception as exc: # noqa: BLE001 - surface runner failures in the UI
169
+ raise gr.Error(f"Smoke test failed: {exc}") from exc
170
+
171
+ summary, rows, download = _view(run)
172
+ choices = _run_options()
173
+ banner = _token_banner()
174
+ return summary, rows, download, gr.update(choices=choices, value=run["run_id"]), banner
175
+
176
+
177
+ def load_history(
178
+ run_id: str | None,
179
+ progress: gr.Progress = gr.Progress(),
180
+ ) -> tuple[str, list[list[Any]], str | None]:
181
+ if not run_id:
182
+ run = smoke.load_latest_run()
183
+ else:
184
+ try:
185
+ run = smoke.load_run(run_id)
186
+ except ValueError as exc:
187
+ raise gr.Error(str(exc)) from exc
188
+ if run is None:
189
+ raise gr.Error("That run could not be found in storage.")
190
+ return _view(run)
191
+
192
+
193
+ with gr.Blocks(title="StockAnalysis - HF Inference Smoke Test") as demo:
194
+ gr.Markdown(
195
+ "# HF Inference Smoke Test\n"
196
+ "Smoke-tests the **30 most trending** chat models on Hugging Face through "
197
+ "Inference Providers. Each model returns a literal **True** or **False**."
198
+ )
199
+
200
+ token_banner = gr.Markdown(_token_banner())
201
+
202
+ with gr.Row():
203
+ model_limit = gr.Slider(
204
+ minimum=1,
205
+ maximum=50,
206
+ value=smoke.DEFAULT_MODEL_LIMIT,
207
+ step=1,
208
+ label="Models to test",
209
+ info="Trending order, keeping only models with a live provider.",
210
+ )
211
+ run_button = gr.Button("Run smoke test", variant="primary", scale=1)
212
+
213
+ progress_note = gr.Markdown("*Idle.*")
214
+ summary = gr.Markdown(_summary(None))
215
+ table = gr.Dataframe(
216
+ headers=TABLE_HEADERS,
217
+ datatype=TABLE_TYPES,
218
+ value=[],
219
+ interactive=False,
220
+ wrap=True,
221
+ label="Results (True = valid chat completion)",
222
+ )
223
+
224
+ with gr.Row():
225
+ history = gr.Dropdown(
226
+ choices=[],
227
+ value=None,
228
+ label="Run history",
229
+ info="Load a previous run from /data/hf-inference-smoke/runs.",
230
+ scale=4,
231
+ )
232
+ download = gr.File(label="Download run JSON", interactive=False, scale=1)
233
+
234
+ with gr.Accordion("How it works", open=False):
235
+ gr.Markdown(HOW_IT_WORKS)
236
+
237
+ run_button.click(
238
+ fn=run_smoke_test,
239
+ inputs=[model_limit],
240
+ outputs=[summary, table, download, history, token_banner],
241
+ ).then(lambda: "*Run finished.*", outputs=progress_note)
242
+
243
+ history.change(
244
+ fn=load_history,
245
+ inputs=[history],
246
+ outputs=[summary, table, download],
247
+ )
248
+
249
+ demo.load(
250
+ fn=initial_view,
251
+ outputs=[summary, table, download, history, token_banner],
252
+ )
253
 
254
+ if __name__ == "__main__":
255
+ demo.launch(theme=gr.themes.Soft())