ashe0042 commited on
Commit
46ffb1e
·
1 Parent(s): 1a8d5dc

Streamlit dashboard: professional dark theme, correct bar charts, example failures

Browse files
Files changed (2) hide show
  1. dashboard/app.py +517 -0
  2. requirements.txt +3 -0
dashboard/app.py ADDED
@@ -0,0 +1,517 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Streamlit dashboard for AusRegBench benchmark results.
3
+
4
+ Reads results/raw_results.jsonl (per (query, config) judged outcomes).
5
+ All percentages shown are computed live from that file, never hardcoded,
6
+ so the dashboard stays correct if the benchmark is re-run.
7
+ """
8
+
9
+ import html
10
+ import json
11
+ from pathlib import Path
12
+
13
+ import pandas as pd
14
+ import plotly.express as px
15
+ import streamlit as st
16
+
17
+ RESULTS_DIR = Path(__file__).resolve().parent.parent / "results"
18
+ RAW_RESULTS_PATH = RESULTS_DIR / "raw_results.jsonl"
19
+
20
+ GITHUB_URL = "https://github.com/AashishPatnaik/AusRegBench"
21
+
22
+ CONFIG_ORDER = ["naive", "hybrid", "rerank", "kg_augmented", "grounded"]
23
+ CONFIG_LABELS = {
24
+ "naive": "Naive",
25
+ "hybrid": "Hybrid",
26
+ "rerank": "Rerank",
27
+ "kg_augmented": "KG-Augmented",
28
+ "grounded": "Grounded",
29
+ }
30
+
31
+ BG = "#0A0E1A"
32
+ CARD_BG = "#131929"
33
+ BORDER = "#1E2D4A"
34
+ ACCENT = "#00C2FF"
35
+ SUCCESS = "#00D68F"
36
+ DANGER = "#FF4B4B"
37
+ WARNING = "#F5A623"
38
+ TEXT_PRIMARY = "#F0F4FF"
39
+ TEXT_MUTED = "#8B9BB4"
40
+
41
+ CONFIG_COLORS = {
42
+ "naive": DANGER,
43
+ "hybrid": WARNING,
44
+ "rerank": WARNING,
45
+ "kg_augmented": ACCENT,
46
+ "grounded": SUCCESS,
47
+ }
48
+ TAXONOMY_BUCKETS = [
49
+ "correct_and_faithful",
50
+ "misstated_obligation",
51
+ "missing_citation",
52
+ "real_but_irrelevant",
53
+ "fabricated_citation",
54
+ ]
55
+ TAXONOMY_LABELS = {
56
+ "correct_and_faithful": "Correct & faithful",
57
+ "misstated_obligation": "Misstated obligation",
58
+ "missing_citation": "Missing citation",
59
+ "real_but_irrelevant": "Real but irrelevant",
60
+ "fabricated_citation": "Fabricated citation",
61
+ }
62
+ TAXONOMY_COLORS = {
63
+ "correct_and_faithful": SUCCESS,
64
+ "misstated_obligation": WARNING,
65
+ "missing_citation": TEXT_MUTED,
66
+ "real_but_irrelevant": ACCENT,
67
+ "fabricated_citation": DANGER,
68
+ }
69
+
70
+ st.set_page_config(page_title="AusRegBench", layout="wide")
71
+
72
+
73
+ def inject_css() -> None:
74
+ st.markdown(
75
+ f"""
76
+ <style>
77
+ .stApp {{
78
+ background-color: {BG};
79
+ color: {TEXT_PRIMARY};
80
+ }}
81
+ html, body, [class*="css"] {{
82
+ font-size: 16px;
83
+ }}
84
+ #MainMenu, header, footer {{ visibility: hidden; }}
85
+
86
+ .hero-title {{
87
+ font-size: 3.4rem;
88
+ font-weight: 800;
89
+ color: {TEXT_PRIMARY};
90
+ letter-spacing: -0.02em;
91
+ margin-bottom: 0.1rem;
92
+ }}
93
+ .hero-subhead {{
94
+ font-size: 1.3rem;
95
+ color: {TEXT_MUTED};
96
+ margin-bottom: 1.8rem;
97
+ }}
98
+ .hero-sentence {{
99
+ font-size: 1.15rem;
100
+ color: {TEXT_PRIMARY};
101
+ margin-top: 1.2rem;
102
+ line-height: 1.6;
103
+ }}
104
+
105
+ .section-title {{
106
+ font-size: 1.7rem;
107
+ font-weight: 700;
108
+ color: {TEXT_PRIMARY};
109
+ margin-top: 2.2rem;
110
+ margin-bottom: 0.3rem;
111
+ }}
112
+ .section-subtitle {{
113
+ font-size: 1.05rem;
114
+ color: {TEXT_MUTED};
115
+ margin-bottom: 1.2rem;
116
+ }}
117
+
118
+ .metric-card {{
119
+ background-color: {CARD_BG};
120
+ border: 1px solid {BORDER};
121
+ border-radius: 12px;
122
+ padding: 1.4rem 1.2rem;
123
+ text-align: center;
124
+ height: 100%;
125
+ }}
126
+ .metric-value {{
127
+ font-size: 2.6rem;
128
+ font-weight: 800;
129
+ line-height: 1.1;
130
+ }}
131
+ .metric-label {{
132
+ font-size: 1rem;
133
+ color: {TEXT_MUTED};
134
+ margin-top: 0.5rem;
135
+ }}
136
+
137
+ .config-card {{
138
+ background-color: {CARD_BG};
139
+ border: 2px solid {BORDER};
140
+ border-radius: 12px;
141
+ padding: 1.2rem 1rem;
142
+ height: 100%;
143
+ }}
144
+ .config-card-name {{
145
+ font-size: 1.2rem;
146
+ font-weight: 700;
147
+ color: {TEXT_PRIMARY};
148
+ margin-bottom: 0.8rem;
149
+ }}
150
+ .config-stat-row {{
151
+ display: flex;
152
+ justify-content: space-between;
153
+ font-size: 1rem;
154
+ margin-bottom: 0.35rem;
155
+ color: {TEXT_PRIMARY};
156
+ }}
157
+ .config-stat-label {{
158
+ color: {TEXT_MUTED};
159
+ }}
160
+
161
+ .badge-red {{
162
+ display: inline-block;
163
+ background-color: rgba(255, 75, 75, 0.15);
164
+ color: {DANGER};
165
+ border: 1px solid {DANGER};
166
+ border-radius: 6px;
167
+ padding: 0.2rem 0.7rem;
168
+ font-size: 0.8rem;
169
+ font-weight: 700;
170
+ letter-spacing: 0.04em;
171
+ margin-bottom: 0.8rem;
172
+ }}
173
+ .example-card {{
174
+ background-color: {CARD_BG};
175
+ border: 1px solid {BORDER};
176
+ border-radius: 12px;
177
+ padding: 1.4rem;
178
+ margin-bottom: 1.4rem;
179
+ }}
180
+ .example-question {{
181
+ font-size: 1.1rem;
182
+ font-weight: 700;
183
+ color: {TEXT_PRIMARY};
184
+ margin: 0.6rem 0 1rem 0;
185
+ }}
186
+ .example-flex {{
187
+ display: flex;
188
+ gap: 1.2rem;
189
+ }}
190
+ .example-box {{
191
+ flex: 1;
192
+ border-radius: 8px;
193
+ padding: 1rem;
194
+ background-color: {BG};
195
+ font-size: 0.95rem;
196
+ color: {TEXT_PRIMARY};
197
+ line-height: 1.5;
198
+ }}
199
+ .example-box-rag {{
200
+ border-left: 3px solid {DANGER};
201
+ }}
202
+ .example-box-reasoning {{
203
+ border-left: 3px solid {ACCENT};
204
+ }}
205
+ .example-box-title {{
206
+ font-size: 0.8rem;
207
+ font-weight: 700;
208
+ letter-spacing: 0.04em;
209
+ color: {TEXT_MUTED};
210
+ margin-bottom: 0.5rem;
211
+ }}
212
+
213
+ .stat-list {{
214
+ background-color: {CARD_BG};
215
+ border: 1px solid {BORDER};
216
+ border-radius: 12px;
217
+ padding: 1.4rem;
218
+ height: 100%;
219
+ }}
220
+ .stat-list-title {{
221
+ font-size: 1.2rem;
222
+ font-weight: 700;
223
+ color: {ACCENT};
224
+ margin-bottom: 0.8rem;
225
+ }}
226
+ .stat-list-item {{
227
+ font-size: 1rem;
228
+ color: {TEXT_PRIMARY};
229
+ margin-bottom: 0.5rem;
230
+ }}
231
+ .stat-list-item b {{
232
+ color: {TEXT_PRIMARY};
233
+ }}
234
+ </style>
235
+ """,
236
+ unsafe_allow_html=True,
237
+ )
238
+
239
+
240
+ @st.cache_data
241
+ def load_raw_results() -> pd.DataFrame:
242
+ records = []
243
+ with open(RAW_RESULTS_PATH) as f:
244
+ for line in f:
245
+ line = line.strip()
246
+ if not line:
247
+ continue
248
+ record = json.loads(line)
249
+ if "taxonomy_bucket" in record:
250
+ records.append(record)
251
+ return pd.DataFrame(records)
252
+
253
+
254
+ def build_metrics_table(df: pd.DataFrame) -> pd.DataFrame:
255
+ rows = []
256
+ for config_name in CONFIG_ORDER:
257
+ config_df = df[df["config_name"] == config_name]
258
+ total = len(config_df)
259
+ bucket_counts = config_df["taxonomy_bucket"].value_counts()
260
+
261
+ def pct(bucket: str) -> float:
262
+ return round(100 * bucket_counts.get(bucket, 0) / total, 1) if total else 0.0
263
+
264
+ rows.append(
265
+ {
266
+ "config_name": config_name,
267
+ "total": total,
268
+ "faithful_pct": pct("correct_and_faithful"),
269
+ "misstated_pct": pct("misstated_obligation"),
270
+ "missing_pct": pct("missing_citation"),
271
+ "fabricated_pct": pct("fabricated_citation"),
272
+ }
273
+ )
274
+ return pd.DataFrame(rows)
275
+
276
+
277
+ def metric_card(value: str, label: str, color: str) -> str:
278
+ return (
279
+ f'<div class="metric-card">'
280
+ f'<div class="metric-value" style="color:{color};">{html.escape(value)}</div>'
281
+ f'<div class="metric-label">{html.escape(label)}</div>'
282
+ f"</div>"
283
+ )
284
+
285
+
286
+ def config_card(row: pd.Series) -> str:
287
+ border_color = (
288
+ SUCCESS if row["config_name"] == "grounded"
289
+ else DANGER if row["config_name"] == "naive"
290
+ else BORDER
291
+ )
292
+ name = CONFIG_LABELS[row["config_name"]]
293
+ return (
294
+ f'<div class="config-card" style="border-color:{border_color};">'
295
+ f'<div class="config-card-name">{html.escape(name)}</div>'
296
+ f'<div class="config-stat-row"><span class="config-stat-label">Faithful</span>'
297
+ f'<b style="color:{SUCCESS};">{row["faithful_pct"]}%</b></div>'
298
+ f'<div class="config-stat-row"><span class="config-stat-label">Misstated</span>'
299
+ f'<b style="color:{WARNING};">{row["misstated_pct"]}%</b></div>'
300
+ f'<div class="config-stat-row"><span class="config-stat-label">Missing citation</span>'
301
+ f'<b style="color:{TEXT_MUTED};">{row["missing_pct"]}%</b></div>'
302
+ f"</div>"
303
+ )
304
+
305
+
306
+ def example_card(row: pd.Series) -> str:
307
+ rag_answer = html.escape(row["rag_answer"][:400]).replace("\n", "<br>")
308
+ reasoning = html.escape(row["layer2_reasoning"])
309
+ question = html.escape(row["question"])
310
+ return (
311
+ f'<div class="example-card">'
312
+ f'<span class="badge-red">MISSTATED OBLIGATION</span>'
313
+ f'<div class="example-question">{question}</div>'
314
+ f'<div class="example-flex">'
315
+ f'<div class="example-box example-box-rag">'
316
+ f'<div class="example-box-title">RAG ANSWER</div>{rag_answer}</div>'
317
+ f'<div class="example-box example-box-reasoning">'
318
+ f'<div class="example-box-title">JUDGE REASONING</div>{reasoning}</div>'
319
+ f"</div></div>"
320
+ )
321
+
322
+
323
+ inject_css()
324
+
325
+ raw_df = load_raw_results()
326
+ metrics_df = build_metrics_table(raw_df)
327
+
328
+ grounded = metrics_df[metrics_df["config_name"] == "grounded"].iloc[0]
329
+ naive = metrics_df[metrics_df["config_name"] == "naive"].iloc[0]
330
+ reduction_pct = (
331
+ round(100 * (naive["misstated_pct"] - grounded["misstated_pct"]) / naive["misstated_pct"])
332
+ if naive["misstated_pct"]
333
+ else 0
334
+ )
335
+
336
+ # --- Hero ---
337
+
338
+ st.markdown('<div class="hero-title">AusRegBench</div>', unsafe_allow_html=True)
339
+ st.markdown(
340
+ '<div class="hero-subhead">RAG Faithfulness Benchmark for Australian Financial Regulation</div>',
341
+ unsafe_allow_html=True,
342
+ )
343
+
344
+ hero_cols = st.columns(3)
345
+ with hero_cols[0]:
346
+ st.markdown(
347
+ metric_card(f'{grounded["faithful_pct"]}%', "Grounded config faithful rate", SUCCESS),
348
+ unsafe_allow_html=True,
349
+ )
350
+ with hero_cols[1]:
351
+ st.markdown(
352
+ metric_card(f'{naive["faithful_pct"]}%', "Naive baseline faithful rate", DANGER),
353
+ unsafe_allow_html=True,
354
+ )
355
+ with hero_cols[2]:
356
+ st.markdown(
357
+ metric_card(f"{reduction_pct}%", "Reduction in misstatements", ACCENT),
358
+ unsafe_allow_html=True,
359
+ )
360
+
361
+ st.markdown(
362
+ f'<div class="hero-sentence">Citation-forcing cuts obligation misstatements by '
363
+ f'{reduction_pct}% — but still misstates in {grounded["misstated_pct"]}% of cases, '
364
+ f"primarily through obligation truncation.</div>",
365
+ unsafe_allow_html=True,
366
+ )
367
+
368
+ # --- Section 1: results by configuration ---
369
+
370
+ st.markdown('<div class="section-title">Results by Configuration</div>', unsafe_allow_html=True)
371
+
372
+ config_cols = st.columns(5)
373
+ for col, (_, row) in zip(config_cols, metrics_df.iterrows()):
374
+ with col:
375
+ st.markdown(config_card(row), unsafe_allow_html=True)
376
+
377
+ # --- Section 2: key finding chart ---
378
+
379
+ st.markdown(
380
+ '<div class="section-title">Faithful answers by RAG configuration (n=119 per config)</div>',
381
+ unsafe_allow_html=True,
382
+ )
383
+
384
+ faithful_fig = px.bar(
385
+ metrics_df,
386
+ x="faithful_pct",
387
+ y="config_name",
388
+ orientation="h",
389
+ color="config_name",
390
+ color_discrete_map=CONFIG_COLORS,
391
+ category_orders={"config_name": CONFIG_ORDER},
392
+ )
393
+ faithful_fig.update_traces(
394
+ texttemplate="%{x:.1f}%", textposition="outside"
395
+ )
396
+ faithful_fig.update_yaxes(
397
+ title="",
398
+ ticktext=[CONFIG_LABELS[c] for c in CONFIG_ORDER],
399
+ tickvals=CONFIG_ORDER,
400
+ )
401
+ faithful_fig.update_xaxes(title="Faithful %", range=[0, 100])
402
+ faithful_fig.add_vline(
403
+ x=grounded["faithful_pct"],
404
+ line_dash="dash",
405
+ line_color=SUCCESS,
406
+ annotation_text=f'Grounded: {grounded["faithful_pct"]}%',
407
+ annotation_position="bottom right",
408
+ annotation_font_color=SUCCESS,
409
+ )
410
+ faithful_fig.update_layout(
411
+ template="plotly_dark",
412
+ plot_bgcolor=CARD_BG,
413
+ paper_bgcolor=BG,
414
+ font_color=TEXT_PRIMARY,
415
+ showlegend=False,
416
+ margin=dict(l=10, r=10, t=10, b=10),
417
+ )
418
+ st.plotly_chart(faithful_fig, use_container_width=True)
419
+
420
+ # --- Section 3: failure breakdown ---
421
+
422
+ st.markdown(
423
+ '<div class="section-title">Failure taxonomy across configurations</div>',
424
+ unsafe_allow_html=True,
425
+ )
426
+
427
+ bucket_counts_df = (
428
+ raw_df.groupby(["config_name", "taxonomy_bucket"]).size().reset_index(name="count")
429
+ )
430
+ bucket_counts_df["taxonomy_label"] = bucket_counts_df["taxonomy_bucket"].map(TAXONOMY_LABELS)
431
+
432
+ taxonomy_fig = px.bar(
433
+ bucket_counts_df,
434
+ x="config_name",
435
+ y="count",
436
+ color="taxonomy_bucket",
437
+ barmode="stack",
438
+ color_discrete_map=TAXONOMY_COLORS,
439
+ category_orders={"config_name": CONFIG_ORDER, "taxonomy_bucket": TAXONOMY_BUCKETS},
440
+ )
441
+ for trace in taxonomy_fig.data:
442
+ bucket_key = trace.name
443
+ trace.name = TAXONOMY_LABELS.get(bucket_key, bucket_key)
444
+ taxonomy_fig.update_xaxes(
445
+ title="",
446
+ ticktext=[CONFIG_LABELS[c] for c in CONFIG_ORDER],
447
+ tickvals=CONFIG_ORDER,
448
+ )
449
+ taxonomy_fig.update_yaxes(title="Queries")
450
+ taxonomy_fig.update_layout(
451
+ template="plotly_dark",
452
+ plot_bgcolor=CARD_BG,
453
+ paper_bgcolor=BG,
454
+ font_color=TEXT_PRIMARY,
455
+ legend_title_text="",
456
+ margin=dict(l=10, r=10, t=10, b=10),
457
+ )
458
+ st.plotly_chart(taxonomy_fig, use_container_width=True)
459
+
460
+ # --- Section 4: example failures ---
461
+
462
+ st.markdown(
463
+ '<div class="section-title">How obligation misstatements happen</div>', unsafe_allow_html=True
464
+ )
465
+ st.markdown(
466
+ '<div class="section-subtitle">Three real examples from the naive config — '
467
+ "same provision, different ways RAG gets it wrong</div>",
468
+ unsafe_allow_html=True,
469
+ )
470
+
471
+ misstated_naive = raw_df[
472
+ (raw_df["config_name"] == "naive") & (raw_df["taxonomy_bucket"] == "misstated_obligation")
473
+ ].head(3)
474
+
475
+ for _, row in misstated_naive.iterrows():
476
+ st.markdown(example_card(row), unsafe_allow_html=True)
477
+
478
+ # --- Section 5: corpus & methodology ---
479
+
480
+ st.markdown(
481
+ '<div class="section-title">Corpus & Methodology</div>', unsafe_allow_html=True
482
+ )
483
+
484
+ method_cols = st.columns(2)
485
+ with method_cols[0]:
486
+ st.markdown(
487
+ f"""
488
+ <div class="stat-list">
489
+ <div class="stat-list-title">Corpus</div>
490
+ <div class="stat-list-item"><b>5</b> sources — Corporations Act 2001,
491
+ Banking Act 1959, CPS 220, CPS 230, CPS 234</div>
492
+ <div class="stat-list-item"><b>11,613</b> clause-aware chunks, paragraph
493
+ IDs preserved</div>
494
+ <div class="stat-list-item"><b>120</b> hand-verified gold queries</div>
495
+ <div class="stat-list-item"><b>5</b> stress strata</div>
496
+ </div>
497
+ """,
498
+ unsafe_allow_html=True,
499
+ )
500
+ with method_cols[1]:
501
+ st.markdown(
502
+ f"""
503
+ <div class="stat-list">
504
+ <div class="stat-list-title">Judge Validation</div>
505
+ <div class="stat-list-item">Cohen's <b>κ = 0.78</b> vs. human labels</div>
506
+ <div class="stat-list-item"><b>24/25</b> agreement on hand-labeled sample</div>
507
+ <div class="stat-list-item">Two-layer pipeline: deterministic citation
508
+ check + LLM entailment judge</div>
509
+ <div class="stat-list-item">Judge model differs from every generator
510
+ model under test</div>
511
+ </div>
512
+ """,
513
+ unsafe_allow_html=True,
514
+ )
515
+
516
+ st.markdown("<br>", unsafe_allow_html=True)
517
+ st.link_button("View source on GitHub", GITHUB_URL)
requirements.txt CHANGED
@@ -2,11 +2,14 @@ anthropic==0.111.0
2
  datasets==3.6.0
3
  numpy==2.4.6
4
  openai==2.42.0
 
5
  pdfplumber==0.11.10
6
  pgvector==0.4.1
 
7
  psycopg2-binary==2.9.10
8
  python-dotenv==1.1.0
9
  requests==2.34.2
10
  scikit-learn==1.9.0
11
  sentence-transformers==3.4.1
 
12
  langsmith
 
2
  datasets==3.6.0
3
  numpy==2.4.6
4
  openai==2.42.0
5
+ pandas==3.0.3
6
  pdfplumber==0.11.10
7
  pgvector==0.4.1
8
+ plotly==6.8.0
9
  psycopg2-binary==2.9.10
10
  python-dotenv==1.1.0
11
  requests==2.34.2
12
  scikit-learn==1.9.0
13
  sentence-transformers==3.4.1
14
+ streamlit
15
  langsmith