milindkamat0507 commited on
Commit
585f15c
·
verified ·
1 Parent(s): 5a242c6

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +296 -0
app.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py — BERTopic Topic Modelling Agent UI.
3
+
4
+ Three UX features:
5
+ 1. Phase banner — large prominent display of current B&C phase
6
+ 2. Dynamic prompts — phase-appropriate suggested next actions
7
+ 3. Auto-populated review table — loads from tool checkpoint files
8
+
9
+ 9-column review table: #, Topic Label, Top Evidence, Sentences, Papers,
10
+ Approve, Rename To, Move To, Reasoning.
11
+ """
12
+
13
+ import gradio as gr
14
+ import pandas as pd
15
+ import json
16
+ import os
17
+ import re
18
+ import tempfile
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+ from agent import run as agent_run
22
+
23
+ THREAD_ID = f"bertopic-{datetime.now().strftime('%Y%m%d%H%M%S')}"
24
+
25
+ REVIEW_COLS = [
26
+ "#", "Topic Label", "Top Evidence", "Sentences", "Papers",
27
+ "Approve", "Rename To", "Move To", "Reasoning",
28
+ ]
29
+
30
+ EMPTY_TABLE = pd.DataFrame(columns=REVIEW_COLS)
31
+
32
+ PHASE_INFO = {
33
+ 0: ("Getting started", "⬜⬜⬜⬜⬜⬜",
34
+ "Upload a CSV file, then click **Analyze my Scopus CSV** and press Send"),
35
+ 1: ("Phase 1 — Familiarisation", "🟦⬜⬜⬜⬜⬜",
36
+ "Click **Run abstract analysis** or **Run title analysis** and press Send"),
37
+ 2: ("Phase 2 — Initial Codes", "🟦🟦⬜⬜⬜⬜",
38
+ "Review clusters in the **Results table** below. Edit Approve / Rename / Move, "
39
+ "then click **Submit Review**"),
40
+ 3: ("Phase 3 — Themes", "🟦🟦🟦⬜⬜⬜",
41
+ "Review merged themes. Edit the table, then click **Submit Review**"),
42
+ 4: ("Phase 4 — Saturation", "🟦🟦🟦🟦⬜⬜",
43
+ "Review saturation metrics. Click **Submit Review** to confirm"),
44
+ 5: ("Phase 5 — Naming", "🟦🟦🟦🟦🟦⬜",
45
+ "Review theme profiles. Edit names, then **Submit Review**"),
46
+ 6: ("Phase 6 — Report", "🟦🟦🟦🟦🟦🟦",
47
+ "Review comparison and narrative. **Submit Review** to finalise"),
48
+ }
49
+
50
+ _path = lambda file: str(
51
+ (hasattr(file, "name") and file.name)
52
+ or (isinstance(file, str) and file)
53
+ or ""
54
+ )
55
+ _name = lambda file: os.path.basename(_path(file))
56
+
57
+
58
+ def _extract_phase(text: str) -> int:
59
+ """Extract phase number from agent response. Returns 0 if not found."""
60
+ found = re.findall(r"Phase (\d)", str(text))
61
+ return int((found or ["0"])[0])
62
+
63
+
64
+ def _phase_banner(num: int) -> str:
65
+ """Generate prominent phase banner with progress bar and next step."""
66
+ name, progress, instruction = PHASE_INFO.get(num, PHASE_INFO[0])
67
+ return (
68
+ f"## {progress} {name}\n\n"
69
+ f"**NEXT STEP →** {instruction}"
70
+ )
71
+
72
+
73
+ def _load_review_table(base_dir: str) -> pd.DataFrame:
74
+ """Load latest checkpoint file into the 9-column review table.
75
+
76
+ Scans base_dir for topic_labels.json, themes.json, taxonomy_alignment.json,
77
+ summaries.json. Loads the most recently modified one and formats it.
78
+ Returns EMPTY_TABLE if nothing found.
79
+ """
80
+ base = Path(str(base_dir or "/tmp/nonexistent_dir_placeholder"))
81
+ candidates = (
82
+ base_dir and base.exists() and sorted(
83
+ (
84
+ list(base.glob("topic_labels.json"))
85
+ + list(base.glob("themes.json"))
86
+ + list(base.glob("taxonomy_alignment.json"))
87
+ + list(base.glob("summaries.json"))
88
+ ),
89
+ key=lambda p: p.stat().st_mtime,
90
+ reverse=True,
91
+ )
92
+ ) or []
93
+
94
+ latest = (candidates[:1] or [None])[0]
95
+ return (latest and [_format_checkpoint(latest)] or [EMPTY_TABLE.copy()])[0]
96
+
97
+
98
+ def _format_checkpoint(path) -> pd.DataFrame:
99
+ """Format a checkpoint JSON file into review table rows."""
100
+ raw = json.loads(Path(path).read_text())
101
+ data = (isinstance(raw, dict) and raw.get("clusters", raw.get("per_theme", []))) or \
102
+ (isinstance(raw, list) and raw) or []
103
+
104
+ def _row(item: dict) -> dict:
105
+ """Map one JSON item to review table columns."""
106
+ return {
107
+ "#": item.get("topic_id", item.get("theme_id", 0)),
108
+ "Topic Label": item.get("label", item.get("theme_label", "")),
109
+ "Top Evidence": str(item.get("representative",
110
+ item.get("notes", "")))[:150],
111
+ "Sentences": item.get("size", item.get("total_papers",
112
+ item.get("papers", 0))),
113
+ "Papers": item.get("size", item.get("total_papers",
114
+ item.get("papers", 0))),
115
+ "Approve": "",
116
+ "Rename To": "",
117
+ "Move To": "",
118
+ "Reasoning": str(item.get("rationale", item.get("notes", ""))),
119
+ }
120
+
121
+ rows = list(map(_row, data[:50]))
122
+ return (rows and [pd.DataFrame(rows, columns=REVIEW_COLS)] or [EMPTY_TABLE.copy()])[0]
123
+
124
+
125
+ def on_file_upload(file):
126
+ """Extract CSV stats and store base directory."""
127
+ path = _path(file)
128
+ result = (not path) and ("Upload a CSV to begin.", "", _phase_banner(0))
129
+ return result or _do_file_upload(path, file)
130
+
131
+
132
+ def _do_file_upload(path: str, file) -> tuple:
133
+ """Actual file processing after path validation."""
134
+ df = pd.read_csv(path)
135
+ rows, cols = df.shape
136
+ base = str(Path(path).parent)
137
+ info = (
138
+ f"**Loaded:** `{_name(file)}`\n\n"
139
+ f"**Shape:** {rows:,} rows x {cols} columns\n\n"
140
+ f"**Columns:** {', '.join(df.columns[:6].tolist())}\n\n"
141
+ f"*Click a prompt below and press Send to begin.*"
142
+ )
143
+ return info, base, _phase_banner(1)
144
+
145
+
146
+ def on_send(user_msg, history, file, base_dir):
147
+ """Pass user message to agent. Update phase banner and review table."""
148
+ msg = (user_msg or "").strip() or "help"
149
+ csv_tag = f"[CSV: {_path(file)}]\n" * bool(file)
150
+
151
+ history = list(history or [])
152
+ history.append({"role": "user", "content": msg})
153
+ history.append({"role": "assistant", "content": "Thinking..."})
154
+ yield history, "", gr.skip(), gr.skip(), gr.skip()
155
+
156
+ reply = agent_run(csv_tag + msg, thread_id=THREAD_ID)
157
+ history[-1] = {"role": "assistant", "content": reply}
158
+
159
+ phase = _extract_phase(reply)
160
+ banner = _phase_banner(phase)
161
+ table = _load_review_table(base_dir)
162
+
163
+ yield history, "", banner, table, base_dir
164
+
165
+
166
+ def on_submit_review(table_df, history, base_dir):
167
+ """Serialise review table edits to agent."""
168
+ history = list(history or [])
169
+ edits = table_df.to_json(orient="records", indent=2)
170
+
171
+ history.append({"role": "user", "content": "[REVIEW SUBMITTED]"})
172
+ history.append({"role": "assistant", "content": "Processing review..."})
173
+
174
+ reply = agent_run(
175
+ "Reviewer submitted table edits.\n\n"
176
+ f"```json\n{edits}\n```\n\n"
177
+ "Process: Approve/Reject decisions, Rename To values, "
178
+ "Move To reassignments (call reassign_sentences if moves exist), "
179
+ "Reasoning notes. Then check STOP gates and proceed.",
180
+ thread_id=THREAD_ID,
181
+ )
182
+ history[-1] = {"role": "assistant", "content": reply}
183
+
184
+ phase = _extract_phase(reply)
185
+ return history, _phase_banner(phase), _load_review_table(base_dir)
186
+
187
+
188
+ def on_download(table_df, history):
189
+ """Export review CSV and chat TXT."""
190
+ csv_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv", prefix="review_")
191
+ table_df.to_csv(csv_tmp.name, index=False)
192
+
193
+ txt_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".txt", prefix="chat_")
194
+ txt_tmp.write(
195
+ "\n\n".join(
196
+ list(map(
197
+ lambda m: f"{m.get('role', '').upper()}: {m.get('content', '')}",
198
+ history or [],
199
+ ))
200
+ ).encode("utf-8")
201
+ )
202
+ txt_tmp.close()
203
+ return [csv_tmp.name, txt_tmp.name]
204
+
205
+
206
+ with gr.Blocks(title="BERTopic Agent") as demo:
207
+
208
+ base_dir_state = gr.State(value="")
209
+
210
+ gr.Markdown("# BERTopic Modelling Agent")
211
+ gr.Markdown(
212
+ "**Braun & Clarke 6-Phase Thematic Analysis** "
213
+ "| 10 Tools | 6 STOP Gates | Cosine Agglomerative Clustering"
214
+ )
215
+
216
+ phase_banner = gr.Markdown(value=_phase_banner(0))
217
+
218
+ gr.Markdown("---\n### Section 1 — Data input")
219
+ with gr.Row():
220
+ with gr.Column(scale=3):
221
+ file_input = gr.File(
222
+ label="Upload Scopus CSV",
223
+ file_types=[".csv"],
224
+ file_count="single",
225
+ )
226
+ with gr.Column(scale=5):
227
+ file_info = gr.Markdown("Upload a CSV to begin.")
228
+
229
+ gr.Markdown("---\n### Section 2 — Agent conversation")
230
+ chatbot = gr.Chatbot(label="BERTopic Agent", height=200)
231
+ with gr.Row():
232
+ msg_box = gr.Textbox(
233
+ placeholder="Type a message or click a prompt below, then press Send",
234
+ show_label=False, scale=7, lines=1,
235
+ )
236
+ send_btn = gr.Button("Send", variant="primary", scale=1)
237
+
238
+ gr.Examples(
239
+ examples=[
240
+ "Analyze my Scopus CSV",
241
+ "Run abstract analysis",
242
+ "Run title analysis",
243
+ "Proceed to next phase",
244
+ "Show corpus statistics",
245
+ ],
246
+ inputs=msg_box,
247
+ label="Quick prompts (click to fill, then press Send)",
248
+ )
249
+
250
+ gr.Markdown("---\n### Section 3 — Results (auto-populated from tool outputs)")
251
+ gr.Markdown(
252
+ "This table fills automatically when the agent runs tools. "
253
+ "Edit **Approve**, **Rename To**, **Move To**, **Reasoning** columns, "
254
+ "then click **Submit Review**."
255
+ )
256
+ review_table = gr.Dataframe(
257
+ value=EMPTY_TABLE,
258
+ headers=REVIEW_COLS,
259
+ datatype=["number", "str", "str", "number", "number",
260
+ "str", "str", "str", "str"],
261
+ column_count=(9, "fixed"),
262
+ interactive=True, wrap=True,
263
+ )
264
+ with gr.Row():
265
+ clear_btn = gr.Button("Clear table", variant="secondary", scale=2)
266
+ sub_btn = gr.Button("Submit Review", variant="primary", scale=4)
267
+
268
+ with gr.Accordion("Download", open=False):
269
+ dl_btn = gr.Button("Generate downloads", variant="primary")
270
+ dl_files = gr.File(label="Downloads", file_count="multiple",
271
+ interactive=False)
272
+
273
+ file_input.change(
274
+ on_file_upload,
275
+ inputs=[file_input],
276
+ outputs=[file_info, base_dir_state, phase_banner],
277
+ )
278
+ send_btn.click(
279
+ on_send,
280
+ inputs=[msg_box, chatbot, file_input, base_dir_state],
281
+ outputs=[chatbot, msg_box, phase_banner, review_table, base_dir_state],
282
+ )
283
+ msg_box.submit(
284
+ on_send,
285
+ inputs=[msg_box, chatbot, file_input, base_dir_state],
286
+ outputs=[chatbot, msg_box, phase_banner, review_table, base_dir_state],
287
+ )
288
+ clear_btn.click(lambda: EMPTY_TABLE.copy(), outputs=[review_table])
289
+ sub_btn.click(
290
+ on_submit_review,
291
+ inputs=[review_table, chatbot, base_dir_state],
292
+ outputs=[chatbot, phase_banner, review_table],
293
+ )
294
+ dl_btn.click(on_download, inputs=[review_table, chatbot], outputs=[dl_files])
295
+
296
+ demo.launch(ssr_mode=False, theme=gr.themes.Soft())