milindkamat0507 commited on
Commit
dc4c3eb
·
verified ·
1 Parent(s): 684bbba

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -413
app.py DELETED
@@ -1,413 +0,0 @@
1
- """app.py — Compact Gradio + Mistral + BERTopic. Version 2.1.0 | 4 April 2026. ZERO for/while/if.
2
-
3
- FUNCTIONS IN THIS FILE:
4
- _latest_output() — Scans /tmp for newest rq4_* file → feeds download button
5
- respond() — Core chat handler: takes message + history + file → yields agent response
6
- gr.Blocks() — One-page UI: header + upload + chatbot + input + download
7
- """
8
- import os
9
- import glob
10
- import gradio as gr
11
- from langchain_mistralai import ChatMistralAI
12
- from langgraph.prebuilt import create_react_agent
13
- from langgraph.checkpoint.memory import MemorySaver
14
- from agent import SYSTEM_PROMPT, get_local_tools
15
-
16
- print(">>> app.py: imports complete")
17
-
18
- # ═══════════════════════════════════════════════
19
- # AGENT SETUP — Mistral brain + 5 BERTopic tools
20
- # ═══════════════════════════════════════════════
21
- llm = ChatMistralAI(model="mistral-small-latest", temperature=0, timeout=300)
22
- tools = get_local_tools()
23
- agent = create_react_agent(model=llm, tools=tools, prompt=SYSTEM_PROMPT, checkpointer=MemorySaver())
24
- print(f">>> app.py: agent ready ({len(tools)} tools, Mistral Large)")
25
-
26
- _msg_count = 0
27
- _uploaded = {"path": ""}
28
-
29
- # ═══════════════════════════════════════════════
30
- # COMPACT HEADER — fits in ~60px
31
- # ═══════════════════════════════════════════════
32
- HEADER_HTML = """
33
- <style>
34
- @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap');
35
- .gradio-container {font-family: 'DM Sans', sans-serif !important; padding: 4px 8px !important; max-width: 100% !important;}
36
- footer {display: none !important;}
37
- .section-box {border: 1px solid #e2e8f0; border-radius: 8px; padding: 8px 12px; margin-bottom: 6px; background: #fafbfc;}
38
- .section-label {font-size: 0.75em; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;}
39
- </style>
40
- <div style="background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%); border-radius: 8px; padding: 8px 16px; margin-bottom: 4px; color: white; display: flex; align-items: center; gap: 10px;">
41
- <span style="font-size: 1.3em;">🔬</span>
42
- <div>
43
- <span style="font-size: 1em; font-weight: 700; color: #e0e0ff;">Topic Modelling — Agentic AI</span>
44
- <span style="font-size: 0.65em; color: #94a3b8; margin-left: 8px;">Mistral 🇫🇷 · Cosine Clustering · 384d · B&C Thematic Analysis</span>
45
- </div>
46
- </div>
47
- """
48
-
49
- # ═══════════════════════════════════════════════
50
- # FUNCTION 1: Find latest output for download
51
- # ═══════════════════════════════════════════════
52
- def _latest_output():
53
- """Scan /tmp for ALL rq4_* files, sorted by phase order.
54
- Returns list of filepaths for gr.File download component."""
55
- # Phase order: summaries → labels → themes → taxonomy → charts → comparison → narrative
56
- phase_order = {"summaries": 1, "labels": 2, "themes": 3, "taxonomy": 4,
57
- "emb": 0, "intertopic": 5, "bars": 6, "hierarchy": 7,
58
- "heatmap": 8, "comparison": 9, "narrative": 10}
59
- files = (glob.glob("/tmp/rq4_*.csv") + glob.glob("/tmp/rq4_*.html")
60
- + glob.glob("/tmp/rq4_*.txt") + glob.glob("/tmp/checkpoints/rq4_*.json"))
61
- # Sort by phase order using filename keywords
62
- scored = list(map(
63
- lambda f: (sum(v * (k in f) for k, v in phase_order.items()), f), files))
64
- scored.sort(key=lambda x: x[0])
65
- return list(map(lambda x: x[1], scored)) or None
66
-
67
-
68
-
69
-
70
- def _build_progress():
71
- """Build HTML progress pipeline showing which B&C phases are complete."""
72
- checks = [
73
- ("① Load", bool(glob.glob("/tmp/checkpoints/rq4_*_summaries.json") or glob.glob("/tmp/checkpoints/rq4_*_emb.npy"))),
74
- ("② Codes", bool(glob.glob("/tmp/checkpoints/rq4_*_labels.json"))),
75
- ("③ Themes", bool(glob.glob("/tmp/checkpoints/rq4_*_themes.json"))),
76
- ("④ Review", bool(glob.glob("/tmp/checkpoints/rq4_*_themes.json"))),
77
- ("⑤ Names", bool(glob.glob("/tmp/checkpoints/rq4_*_themes.json"))),
78
- ("⑤½ PAJAIS", bool(glob.glob("/tmp/checkpoints/rq4_*_taxonomy_map.json"))),
79
- ("⑥ Report", bool(glob.glob("/tmp/rq4_comparison.csv") or glob.glob("/tmp/rq4_narrative.txt"))),
80
- ]
81
- done_bg = "#22c55e"
82
- todo_bg = "#e2e8f0"
83
- done_fg = "white"
84
- todo_fg = "#64748b"
85
- items = " → ".join(list(map(
86
- lambda c: (
87
- "<span style='padding:3px 8px;border-radius:4px;"
88
- + "background:" + (done_bg * c[1] or todo_bg) + ";"
89
- + "color:" + (done_fg * c[1] or todo_fg) + ";"
90
- + "font-weight:600;font-size:0.8em;'>"
91
- + c[0] + " " + ("✅" * c[1] or "⬜") + "</span>"
92
- ), checks)))
93
- return "<div style='padding:6px 0;text-align:center;'>" + items + "</div>"
94
-
95
-
96
- # ═══════════════════════════════════════════════
97
- # FUNCTION 2: Chat handler — core of the app
98
- # ═══════════════════════════════════════════════
99
- def respond(message, chat_history, uploaded_file):
100
- """Handle one chat turn:
101
- 1. Store uploaded file path (if new upload)
102
- 2. Append file context to message so agent knows where CSV is
103
- 3. Show progress bubble immediately (user sees instant feedback)
104
- 4. Invoke agent (Mistral brain decides which tools to call)
105
- 5. Replace progress bubble with agent's actual response
106
- 6. Update download link to latest output file
107
-
108
- Uses single thread_id="session" so agent remembers across turns.
109
- Agent asks clarification FIRST (via SYSTEM_PROMPT) before running heavy tools."""
110
- global _msg_count
111
- _msg_count += 1
112
-
113
- # Store file path — no if/else, uses `or` short-circuit
114
- _uploaded["path"] = uploaded_file or _uploaded.get("path", "")
115
-
116
- # Guard: tell agent when no file uploaded (prevents hallucinated filepath)
117
- file_note = (f"\n[CSV file at: {_uploaded['path']}]" * bool(_uploaded["path"])
118
- ) or "\n[No CSV uploaded yet — ask user to upload a file first]"
119
-
120
- # Context: tell agent what phase we're in based on checkpoint files
121
- phase_context = (
122
- "\n[Phase context: labels exist]" * bool(glob.glob("/tmp/checkpoints/rq4_*_labels.json"))
123
- or "\n[Phase context: embeddings exist]" * bool(glob.glob("/tmp/checkpoints/rq4_*_emb.npy"))
124
- or "\n[Phase context: fresh start]"
125
- )
126
-
127
- text = ((message or "").strip() or "Analyze my Scopus CSV") + file_note + phase_context
128
- print(f"\n{'='*60}\n>>> MSG #{_msg_count}: '{text[:120]}'\n{'='*60}")
129
-
130
- # Yield progress bubble immediately — user sees instant response
131
- chat_history = chat_history + [
132
- {"role": "user", "content": (message or "").strip()},
133
- {"role": "assistant", "content": "🔬 **Working...** _Agent is thinking..._"},
134
- ]
135
- yield chat_history, "", _latest_output()
136
-
137
- # Shared session — agent remembers across messages (loaded CSV, phase, etc.)
138
- # handle_tool_error=True on all tools prevents session poisoning from failures
139
- result = agent.invoke(
140
- {"messages": [("human", text)]},
141
- config={"configurable": {"thread_id": "session"}},
142
- )
143
- response = result["messages"][-1].content
144
- print(f">>> Response ({len(response)} chars)")
145
-
146
- # Replace progress bubble with actual response
147
- chat_history[-1] = {"role": "assistant", "content": response}
148
- yield chat_history, "", _latest_output()
149
-
150
-
151
- # ═══════════════════════════════════════════════
152
- # UI LAYOUT — Everything fits ONE screen (~500px)
153
- #
154
- # ┌────────────────────────────────────┐
155
- # │ 🔬 BERTopic Agent (compact) │ ~55px
156
- # ├────────────────────────────────────┤
157
- # │ [📂 Upload CSV] │ ~35px
158
- # ├────────────────────────────────────┤
159
- # │ 💬 Chat bubbles (cloud style) │
160
- # │ User: "Analyze my data" │ ~320px
161
- # │ Agent: "I found 1390 papers. │
162
- # │ Which config? All 3?" │
163
- # ├────────────────────────────────────┤
164
- # │ [Type message... ] [⏎] │ ~35px
165
- # ├────────────────────────────────────┤
166
- # │ 📥 Download │ ~35px
167
- # └────────────────────────────────────┘
168
- # TOTAL: ~480px — fits any screen
169
- # ═══════════════════════════════════════════════
170
- print(">>> Building UI...")
171
- with gr.Blocks(title="Topic Modelling — Agentic AI") as demo:
172
- gr.HTML(HEADER_HTML)
173
-
174
- # ══════════════════════════════════════════
175
- # SECTION 1: DATA INPUT
176
- # ══════════════════════════════════════════
177
- gr.HTML('<div class="section-label">① DATA INPUT</div>')
178
- with gr.Group():
179
- with gr.Row():
180
- upload = gr.File(label="���� Upload Scopus CSV", file_types=[".csv"])
181
- gr.Markdown("**Upload your CSV** then type `run abstract only` in the chat below")
182
-
183
- # ══════════════════════════════════════════
184
- # PHASE PROGRESS PIPELINE
185
- # ══════════════════════════════════════════
186
- phase_progress = gr.HTML(value=_build_progress())
187
-
188
- # ══════════════════════════════════════════
189
- # SECTION 2: AGENT CONVERSATION
190
- # ══════════════════════════════════════════
191
- gr.HTML('<div class="section-label">② AGENT CONVERSATION — follow the prompts below</div>')
192
- with gr.Group():
193
- chatbot = gr.Chatbot(height=250, show_label=False,
194
- placeholder="Upload your Scopus CSV above, then type: run abstract only")
195
- with gr.Row():
196
- msg = gr.Textbox(
197
- placeholder="run · approve · show topic 4 papers · group 0 1 5 · done",
198
- show_label=False, scale=9, lines=1, max_lines=1, container=False)
199
- send = gr.Button("Send", variant="primary", scale=1, min_width=70)
200
-
201
- # ══════════════════════════════════════════
202
- # SECTION 3: RESULTS
203
- # ══════════════════════════════════════════
204
- gr.HTML('<div class="section-label">③ RESULTS — review table, charts, downloads</div>')
205
- with gr.Group():
206
- with gr.Tabs():
207
- with gr.Tab("📋 Review Table"):
208
- gr.Markdown("*Edit Approve / Rename To / Your Reasoning → click Submit. "
209
- "Type `show topic 4 papers` in chat to see paper list.*")
210
- review_table = gr.Dataframe(
211
- headers=["#", "Topic Label", "Top Evidence Sentence",
212
- "Sentences", "Papers", "Approve", "Rename To", "Your Reasoning"],
213
- datatype=["number", "str", "str", "number", "number", "str", "str", "str"],
214
- column_count=(8, "fixed"),
215
- interactive=True,
216
- row_count=(1, "dynamic"),
217
- )
218
- submit_review = gr.Button("✅ Submit Review to Agent", variant="primary")
219
-
220
- # Paper viewer — select topic to see papers
221
- gr.Markdown("---")
222
- gr.Markdown("**📄 View papers in a topic:**")
223
- with gr.Row():
224
- topic_num = gr.Number(label="Topic #", value=0, precision=0, minimum=0, scale=1)
225
- view_papers_btn = gr.Button("Show Papers", scale=1)
226
- paper_list = gr.Textbox(label="Papers in selected topic", lines=8,
227
- interactive=False)
228
-
229
- with gr.Tab("📊 Charts"):
230
- chart_selector = gr.Dropdown(choices=[], label="Select Chart", interactive=True)
231
- chart_display = gr.HTML(
232
- value="<div style='height:350px;display:flex;align-items:center;justify-content:center;"
233
- "color:#94a3b8;border:1px dashed #cbd5e1;border-radius:8px;'>"
234
- "Charts appear after BERTopic runs</div>")
235
-
236
- with gr.Tab("📥 Download"):
237
- gr.Markdown(
238
- "**Files by Phase (per run: abstract / title):**\n\n"
239
- "**Phase 2 — Discovery:** `summaries.json` (raw topics) · `emb.npy` (embeddings)\n\n"
240
- "**Phase 2 — Labeling:** `labels.json` (Mistral-labeled topics)\n\n"
241
- "**Phase 2 — Charts:** `intertopic.html` · `bars.html` · `hierarchy.html` · `heatmap.html`\n\n"
242
- "**Phase 3 — Themes:** `themes.json` (consolidated themes)\n\n"
243
- "**Phase 5.5 — Taxonomy:** `taxonomy_map.json` (PAJAIS mapped vs NOVEL)\n\n"
244
- "**Phase 6 — Report:** `comparison.csv` (abstract vs title) · `narrative.txt` (500-word draft)"
245
- )
246
- download = gr.File(label="All output files", file_count="multiple")
247
-
248
- def _load_chart(chart_name):
249
- """Load chart HTML into iframe — gr.HTML strips scripts, iframe executes them."""
250
- import html as html_mod
251
- path = f"/tmp/{chart_name}"
252
- content = os.path.exists(path) and open(path).read() or ""
253
- escaped = html_mod.escape(content) * bool(content)
254
- return (f'<iframe srcdoc="{escaped}" width="100%" height="450" '
255
- f'frameborder="0" sandbox="allow-scripts allow-same-origin"></iframe>'
256
- ) * bool(escaped) or "<div style='padding:20px;color:#94a3b8;'>Select a chart above</div>"
257
-
258
- def _get_chart_choices():
259
- """Find all rq4_*.html chart files in /tmp."""
260
- files = sorted(glob.glob("/tmp/rq4_*.html"))
261
- return list(map(os.path.basename, files))
262
-
263
- def _load_review_table():
264
- """Load review table from latest phase JSON.
265
- Priority: taxonomy_map > themes > labels > summaries.
266
- Returns 8-column rows: #, Label, Evidence/Mapping, Sent, Papers, Approve, Rename, Reasoning"""
267
- import json
268
- taxonomy_files = sorted(glob.glob("/tmp/checkpoints/rq4_*_taxonomy_map.json"))
269
- theme_files = sorted(glob.glob("/tmp/checkpoints/rq4_*_themes.json"))
270
- label_files = sorted(glob.glob("/tmp/checkpoints/rq4_*_labels.json"))
271
- summary_files = sorted(glob.glob("/tmp/checkpoints/rq4_*_summaries.json"))
272
-
273
- # Determine which file to load
274
- path = ((taxonomy_files and taxonomy_files[-1])
275
- or (theme_files and theme_files[-1])
276
- or (label_files and label_files[-1])
277
- or (summary_files and summary_files[-1]) or "")
278
- is_taxonomy = bool(taxonomy_files and taxonomy_files[-1] == path)
279
- data = (os.path.exists(path) and json.load(open(path))) or []
280
-
281
- # For taxonomy: merge with themes to get sentence/paper counts
282
- theme_lookup = {}
283
- (is_taxonomy and theme_files) and theme_lookup.update({
284
- t.get("label", ""): t for t in json.load(open(theme_files[-1]))})
285
-
286
- rows = list(map(
287
- lambda pair: [
288
- pair[0],
289
- pair[1].get("label", pair[1].get("top_words", "")[:60]),
290
- # Evidence column: show PAJAIS mapping for taxonomy, evidence sentence otherwise
291
- (is_taxonomy and f"→ {pair[1].get('pajais_match', '?')} | {pair[1].get('reasoning', '')}"[:120])
292
- or (pair[1].get("nearest", [{}])[0].get("sentence", "")[:120] + "...") * bool(pair[1].get("nearest")),
293
- # Sentence/paper counts: from taxonomy lookup or direct
294
- theme_lookup.get(pair[1].get("label", ""), pair[1]).get("sentence_count", pair[1].get("sentence_count", 0)),
295
- theme_lookup.get(pair[1].get("label", ""), pair[1]).get("paper_count", pair[1].get("paper_count", 0)),
296
- "yes",
297
- "",
298
- "",
299
- ], enumerate(data)))
300
- return rows or [[0, "No data yet", "", 0, 0, "", "", ""]]
301
-
302
- def _show_papers(topic_id):
303
- """Show 5 nearest centroid sentences (evidence) + all paper titles for selected topic."""
304
- import json
305
- topic_id = int(topic_id)
306
- summary_files = sorted(glob.glob("/tmp/checkpoints/rq4_*_summaries.json"))
307
- label_files = sorted(glob.glob("/tmp/checkpoints/rq4_*_labels.json"))
308
- all_files = label_files or summary_files
309
- source_labels = list(map(
310
- lambda f: os.path.basename(f).split("_")[1], all_files))
311
- all_data = list(map(lambda f: json.load(open(f)), all_files))
312
-
313
- lines = []
314
- list(map(lambda pair: list(map(
315
- lambda t: (t.get("topic_id") == topic_id) and lines.append(
316
- f"═══ {pair[0].upper()} — Topic {topic_id}: "
317
- f"{t.get('label', t.get('top_words','')[:50])} ═══\n"
318
- f"{t.get('sentence_count', 0)} sentences from {t.get('paper_count', 0)} papers\n"
319
- f"AI Reasoning: {t.get('reasoning', 'not yet labeled')}\n\n"
320
- f"── 5 NEAREST CENTROID SENTENCES (evidence) ──\n"
321
- + "\n".join(list(map(
322
- lambda i: f" {i+1}. \"{t['nearest'][i]['sentence'][:200]}\"\n"
323
- f" Paper: {t['nearest'][i].get('title', '')[:100]}",
324
- range(min(5, len(t.get('nearest', [])))))))
325
- + "\n\n── ALL PAPER TITLES ──\n"
326
- + "\n".join(list(map(
327
- lambda i: f" {i+1}. {t['paper_titles'][i]}",
328
- range(len(t.get('paper_titles', []))))))
329
- ),
330
- pair[1])),
331
- zip(source_labels, all_data)))
332
-
333
- return "\n\n".join(lines) or f"Topic {topic_id} not found."
334
-
335
- view_papers_btn.click(_show_papers, [topic_num], [paper_list])
336
-
337
- def _submit_review(table_data, chat_history):
338
- """Convert edited review table into a message for the agent.
339
- Researcher's edits (approve/rename/reasoning) become natural language."""
340
- rows = table_data.values.tolist()
341
- lines = list(map(
342
- lambda r: (
343
- f"Topic {int(r[0])}: "
344
- + (f"RENAME to '{r[6]}'" * bool(str(r[6]).strip()))
345
- + (f"APPROVE '{r[1]}'" * (not bool(str(r[6]).strip())) * (str(r[5]).lower().startswith("y")))
346
- + (f"REJECT" * (str(r[5]).lower().startswith("n")))
347
- + (f" — reason: {r[7]}" * bool(str(r[7]).strip()))
348
- ), rows))
349
- review_msg = "Review decisions:\n" + "\n".join(lines)
350
- print(f">>> Review submitted: {review_msg[:200]}")
351
-
352
- chat_history = chat_history + [
353
- {"role": "user", "content": review_msg},
354
- {"role": "assistant", "content": "🔬 **Processing review decisions...**"},
355
- ]
356
- yield chat_history, _latest_output(), gr.update(), gr.update(), gr.update(), _build_progress()
357
-
358
- result = agent.invoke(
359
- {"messages": [("human", review_msg)]},
360
- config={"configurable": {"thread_id": "session"}},
361
- )
362
- response = result["messages"][-1].content
363
- chat_history[-1] = {"role": "assistant", "content": response}
364
- # Reload table with updated themes/labels
365
- table_data = _load_review_table()
366
- yield (chat_history, _latest_output(),
367
- gr.update(choices=_get_chart_choices()), gr.update(),
368
- gr.update(value=table_data), _build_progress())
369
-
370
- chart_selector.change(_load_chart, [chart_selector], [chart_display])
371
- submit_review.click(_submit_review, [review_table, chatbot],
372
- [chatbot, download, chart_selector, chart_display, review_table, phase_progress])
373
-
374
- def respond_with_viz(message, chat_history, uploaded_file):
375
- """Wrap respond() and update chart dropdown + review table after each message."""
376
- gen = respond(message, chat_history, uploaded_file)
377
- # First yield (progress)
378
- hist, txt, dl = next(gen)
379
- yield hist, txt, dl, gr.update(choices=_get_chart_choices()), gr.update(), gr.update(), _build_progress()
380
- # Second yield (final response + populate table + charts)
381
- hist, txt, dl = next(gen)
382
- choices = _get_chart_choices()
383
- first_chart = (choices and _load_chart(choices[-1])) or gr.update()
384
- table_data = _load_review_table()
385
- yield (hist, txt, dl,
386
- gr.update(choices=choices, value=(choices and choices[-1]) or None),
387
- first_chart,
388
- gr.update(value=table_data), _build_progress())
389
-
390
- msg.submit(respond_with_viz, [msg, chatbot, upload],
391
- [chatbot, msg, download, chart_selector, chart_display, review_table, phase_progress])
392
- send.click(respond_with_viz, [msg, chatbot, upload],
393
- [chatbot, msg, download, chart_selector, chart_display, review_table, phase_progress])
394
-
395
- def _auto_load_csv(uploaded_file, chat_history):
396
- """Auto-trigger analysis when CSV is uploaded — stats appear without typing."""
397
- gen = respond("Analyze my Scopus CSV", chat_history, uploaded_file)
398
- hist, txt, dl = next(gen)
399
- yield hist, dl, gr.update(), gr.update(), gr.update(), _build_progress()
400
- hist, txt, dl = next(gen)
401
- choices = _get_chart_choices()
402
- first_chart = (choices and _load_chart(choices[-1])) or gr.update()
403
- table_data = _load_review_table()
404
- yield (hist, dl,
405
- gr.update(choices=choices, value=(choices and choices[-1]) or None),
406
- first_chart,
407
- gr.update(value=table_data), _build_progress())
408
-
409
- upload.change(_auto_load_csv, [upload, chatbot],
410
- [chatbot, download, chart_selector, chart_display, review_table, phase_progress])
411
-
412
- print(">>> Launching...")
413
- demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)