benchaffe commited on
Commit
c28ca9d
·
verified ·
1 Parent(s): 16234cb

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +207 -0
app.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, json, html, traceback
2
+ import gradio as gr
3
+ import pandas as pd
4
+ import spaces
5
+ from google import genai
6
+ from google.genai import types
7
+
8
+ import judge_prompts as J
9
+ import trust_analysis as ta
10
+
11
+ # ------------------------------------------------------------------ config
12
+ GEMINI_KEY = os.environ.get("GEMINI_API_KEY")
13
+ APP_PASSWORD = os.environ.get("APP_PASSWORD", "")
14
+ MODEL = os.environ.get("JUDGE_MODEL", "gemini-3.1-flash-lite")
15
+
16
+ MAX_SOURCE_CHARS = 50000
17
+ MAX_SUMMARY_CHARS = 6000
18
+ SESSION_LIMIT = 15
19
+
20
+ client = genai.Client(api_key=GEMINI_KEY) if GEMINI_KEY else None
21
+
22
+ DIMENSIONS = {
23
+ "Faithfulness": ("faithfulness", "faithfulness_score", "unsupported_claims", ("span","type","explanation")),
24
+ "Overclaiming": ("overclaiming", "overclaiming_score", "overclaims", ("span","type","explanation")),
25
+ "Uncertainty preservation": ("uncertainty", "uncertainty_score", "dropped_hedges", ("rendered_as","source_hedge",None)),
26
+ "Methods & limitations": ("methods_limits", "methods_limitations_score", None, None),
27
+ }
28
+ ALL_DIMS = list(DIMENSIONS)
29
+
30
+
31
+ # ------------------------------------------------------------------ judge
32
+ @spaces.GPU
33
+ def _zeroGPU_dummy():
34
+ return True
35
+
36
+ def parse_json(text):
37
+ obj, _ = json.JSONDecoder().raw_decode(text.strip())
38
+ return obj
39
+
40
+ def judge(dimension, task, source, output):
41
+ prompt = J.build_prompt(dimension, task, source, output)
42
+ resp = client.models.generate_content(
43
+ model=MODEL, contents=prompt,
44
+ config=types.GenerateContentConfig(
45
+ system_instruction=J.SYSTEM_PROMPT, temperature=0,
46
+ response_mime_type="application/json"))
47
+ return parse_json(resp.text)
48
+
49
+ def screen(output):
50
+ df = ta.screen_validity(pd.DataFrame([{"generated_output": output,
51
+ "has_output": bool(str(output).strip())}]))
52
+ row = df.iloc[0]
53
+ return bool(row["is_valid"]), str(row["degeneracy_type"])
54
+
55
+ # ------------------------------------------------------------------ rendering
56
+ def esc(s): return html.escape(str(s))
57
+
58
+ def score_colour(s):
59
+ try: s = float(s)
60
+ except: return "#888"
61
+ return "#b5523f" if s <= 2 else "#c9a227" if s < 4 else "#2e8f6a"
62
+
63
+ def highlight(summary, spans):
64
+ out = esc(summary)
65
+ for sp in sorted({s for s in spans if s and len(str(s)) > 4}, key=len, reverse=True):
66
+ out = out.replace(esc(sp), f"<mark style='background:#ffe0b3;color:{TEXT}'>{esc(sp)}</mark>")
67
+ return out.replace("\n", "<br>")
68
+
69
+ TEXT = "#1a1a1a"
70
+ MUTED = "#4b5563"
71
+
72
+ def render_scores(results, valid, degen):
73
+ """Middle column: validity banner + one box per scored dimension."""
74
+ parts = []
75
+ if not valid:
76
+ parts.append(f"<div style='padding:10px;border-left:4px solid #b5523f;background:#fbeae6;"
77
+ f"color:{TEXT}'>"
78
+ f"<b>Validity screen: DEGENERATE</b> — flagged as <code>{esc(degen)}</code>. "
79
+ f"Fault-absence dimensions (overclaiming, uncertainty) are not scored for "
80
+ f"non-functional output; faithfulness is reported as minimal.</div>")
81
+ else:
82
+ parts.append(f"<div style='padding:8px;border-left:4px solid #2e8f6a;background:#eaf6f0;"
83
+ f"color:{TEXT}'><b>Validity screen: functional output</b></div>")
84
+
85
+ all_spans = []
86
+ for label, res in results.items():
87
+ key, sfield, ifield, ikeys = DIMENSIONS[label]
88
+ if "error" in res:
89
+ parts.append(f"<div style='margin-top:12px;color:{TEXT}'><b>{esc(label)}</b>: "
90
+ f"<span style='color:#b5523f'>could not score ({esc(res['error'])})</span></div>")
91
+ continue
92
+ score = res.get(sfield, "–")
93
+ col = score_colour(score)
94
+ head = (f"<div style='margin-top:14px;padding:8px;border:1px solid #e3e7ee;border-radius:8px;"
95
+ f"background:#ffffff;color:{TEXT}'>"
96
+ f"<span style='font-size:15px;font-weight:700'>{esc(label)}</span> "
97
+ f"<span style='float:right;font-weight:700;color:{col}'>{esc(score)} / 5</span>")
98
+ body = ""
99
+ if label == "Methods & limitations":
100
+ flags = [("Methods", res.get("methods_present")),
101
+ ("Limitations", res.get("limitations_present")),
102
+ ("Population", res.get("population_present"))]
103
+ body += f"<div style='margin-top:6px;font-size:13px;color:{TEXT}'>" + " · ".join(
104
+ f"{n}: {'✓' if v else '✗'}" for n, v in flags) + "</div>"
105
+ elif ifield:
106
+ issues = res.get(ifield) or []
107
+ if issues:
108
+ body += f"<ul style='margin:6px 0 0 0;font-size:13px;color:{TEXT}'>"
109
+ for it in issues:
110
+ if not isinstance(it, dict): continue
111
+ span = it.get(ikeys[0], "")
112
+ all_spans.append(span)
113
+ typ = it.get(ikeys[1], "") if ikeys[1] else ""
114
+ exp = it.get(ikeys[2], "") if ikeys[2] else ""
115
+ line = f"<li><i>“{esc(span)}”</i>"
116
+ if typ: line += f" — <code>{esc(typ)}</code>"
117
+ if exp: line += f": {esc(exp)}"
118
+ body += line + "</li>"
119
+ body += "</ul>"
120
+ else:
121
+ body += f"<div style='margin-top:6px;font-size:13px;color:#2e8f6a'>No issues flagged.</div>"
122
+ parts.append(head + body + "</div>")
123
+
124
+ html_out = "<div style='font-family:system-ui;color:" + TEXT + "'>" + "".join(parts) + "</div>"
125
+ return html_out, all_spans
126
+
127
+
128
+ def render_summary(summary, all_spans):
129
+ hi = highlight(summary, all_spans)
130
+ return (f"<div style='font-family:system-ui;color:{TEXT}'>"
131
+ f"<div style='padding:10px;background:#fbfaf7;border:1px solid #ece7dc;border-radius:8px;"
132
+ f"font-size:14px;line-height:1.6;color:{TEXT}'>{hi}</div></div>")
133
+
134
+ # ------------------------------------------------------------------ main callback
135
+ def _err(msg, remaining):
136
+ return f"<b style='color:#b5523f'>{esc(msg)}</b>", "", remaining
137
+
138
+ def evaluate(source, summary, task, dims, remaining):
139
+ if client is None:
140
+ return _err("Server not configured: GEMINI_API_KEY secret is missing.", remaining)
141
+ if remaining <= 0:
142
+ return _err("Session evaluation limit reached. Reload the page to reset.", remaining)
143
+ source, summary = (source or "").strip(), (summary or "").strip()
144
+ if not source or not summary:
145
+ return _err("Please provide both a source text and an AI summary.", remaining)
146
+ if len(source) > MAX_SOURCE_CHARS:
147
+ return _err(f"Source too long ({len(source)} chars; max {MAX_SOURCE_CHARS}). Please trim.", remaining)
148
+ if len(summary) > MAX_SUMMARY_CHARS:
149
+ return _err(f"Summary too long ({len(summary)} chars; max {MAX_SUMMARY_CHARS}). Please trim.", remaining)
150
+ if not dims:
151
+ return _err("Select at least one dimension to run.", remaining)
152
+
153
+ task_key = "structured" if task.startswith("Structured") else "lay"
154
+ valid, degen = screen(summary)
155
+
156
+ results = {}
157
+ for label in dims:
158
+ key, sfield, *_ = DIMENSIONS[label]
159
+ # gate fault-absence dims on validity
160
+ if not valid and key in ("overclaiming", "uncertainty"):
161
+ continue
162
+ if not valid and key in ("faithfulness", "methods_limits"):
163
+ results[label] = {sfield: 1}
164
+ continue
165
+ try:
166
+ results[label] = judge(key, task_key, source, summary)
167
+ except Exception as e:
168
+ results[label] = {"error": str(e)[:120]}
169
+
170
+ scores_html, all_spans = render_scores(
171
+ {k: v for k, v in results.items() if k in DIMENSIONS}, valid, degen)
172
+ summary_html = render_summary(summary, all_spans)
173
+ return scores_html, summary_html, remaining - 1
174
+
175
+ # ------------------------------------------------------------------ UI
176
+ CSS = ".gradio-container{max-width:1450px!important}"
177
+ with gr.Blocks(title="Trust Evaluation Toolkit") as demo:
178
+ gr.Markdown("## Biomedical Summary Trustworthiness Toolkit\n"
179
+ "Scores an AI-generated summary against a validated multi-dimensional trust rubric. "
180
+ "Paste the source article and the AI summary, choose the register and dimensions, and run.")
181
+ remaining = gr.State(SESSION_LIMIT)
182
+ with gr.Row():
183
+ with gr.Column(scale=4):
184
+ source = gr.Textbox(label="Source text (the article)", lines=12, max_lines=20)
185
+ summary = gr.Textbox(label="AI-generated summary", lines=8, max_lines=14)
186
+ task = gr.Radio(["Structured summary", "Lay summary"], value="Structured summary",
187
+ label="Register")
188
+ dims = gr.CheckboxGroup(ALL_DIMS, value=ALL_DIMS, label="Trust dimensions (LLM-judge)")
189
+ btn = gr.Button("Evaluate", variant="primary")
190
+ with gr.Column(scale=3):
191
+ gr.Markdown("**Scores**")
192
+ scores_out = gr.HTML()
193
+ with gr.Column(scale=3):
194
+ gr.Markdown("**Summary with flagged spans**")
195
+ summary_out = gr.HTML()
196
+ btn.click(evaluate, [source, summary, task, dims, remaining], [scores_out, summary_out, remaining])
197
+ gr.Markdown("<small>Scores are produced by an LLM judge validated against human ratings; they are "
198
+ "comparative, not absolute ground truth. Access is rate-limited per session.</small>")
199
+
200
+ def check_auth(username, password):
201
+ return bool(APP_PASSWORD) and password == APP_PASSWORD
202
+
203
+ demo.launch(
204
+ auth=check_auth,
205
+ auth_message="Enter the shared access password.",
206
+ css=CSS,
207
+ )