milindkamat0507 commited on
Commit
dee732b
·
verified ·
1 Parent(s): 77cc7bd

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -349
app.py DELETED
@@ -1,349 +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 (CSV, HTML, JSON, TXT).
54
- Returns list of filepaths for gr.File download component."""
55
- files = sorted(
56
- glob.glob("/tmp/rq4_*.csv") + glob.glob("/tmp/rq4_*.html") + glob.glob("/tmp/rq4_*.txt")
57
- + glob.glob("/tmp/checkpoints/rq4_*.json"),
58
- key=os.path.getmtime, reverse=True)
59
- return files or None
60
-
61
-
62
- # ═══════════════════════════════════════════════
63
- # FUNCTION 2: Chat handler — core of the app
64
- # ═══════════════════════════════════════════════
65
- def respond(message, chat_history, uploaded_file):
66
- """Handle one chat turn:
67
- 1. Store uploaded file path (if new upload)
68
- 2. Append file context to message so agent knows where CSV is
69
- 3. Show progress bubble immediately (user sees instant feedback)
70
- 4. Invoke agent (Mistral brain decides which tools to call)
71
- 5. Replace progress bubble with agent's actual response
72
- 6. Update download link to latest output file
73
-
74
- Uses single thread_id="session" so agent remembers across turns.
75
- Agent asks clarification FIRST (via SYSTEM_PROMPT) before running heavy tools."""
76
- global _msg_count
77
- _msg_count += 1
78
-
79
- # Store file path — no if/else, uses `or` short-circuit
80
- _uploaded["path"] = uploaded_file or _uploaded.get("path", "")
81
-
82
- # Guard: tell agent when no file uploaded (prevents hallucinated filepath)
83
- file_note = (f"\n[CSV file at: {_uploaded['path']}]" * bool(_uploaded["path"])
84
- ) or "\n[No CSV uploaded yet — ask user to upload a file first]"
85
-
86
- # Context: tell agent what phase we're in based on checkpoint files
87
- phase_context = (
88
- "\n[Phase context: labels exist]" * bool(glob.glob("/tmp/checkpoints/rq4_*_labels.json"))
89
- or "\n[Phase context: embeddings exist]" * bool(glob.glob("/tmp/checkpoints/rq4_*_emb.npy"))
90
- or "\n[Phase context: fresh start]"
91
- )
92
-
93
- text = ((message or "").strip() or "Analyze my Scopus CSV") + file_note + phase_context
94
- print(f"\n{'='*60}\n>>> MSG #{_msg_count}: '{text[:120]}'\n{'='*60}")
95
-
96
- # Yield progress bubble immediately — user sees instant response
97
- chat_history = chat_history + [
98
- {"role": "user", "content": (message or "").strip()},
99
- {"role": "assistant", "content": "🔬 **Working...** _Agent is thinking..._"},
100
- ]
101
- yield chat_history, "", _latest_output()
102
-
103
- # Shared session — agent remembers across messages (loaded CSV, phase, etc.)
104
- # handle_tool_error=True on all tools prevents session poisoning from failures
105
- result = agent.invoke(
106
- {"messages": [("human", text)]},
107
- config={"configurable": {"thread_id": "session"}},
108
- )
109
- response = result["messages"][-1].content
110
- print(f">>> Response ({len(response)} chars)")
111
-
112
- # Replace progress bubble with actual response
113
- chat_history[-1] = {"role": "assistant", "content": response}
114
- yield chat_history, "", _latest_output()
115
-
116
-
117
- # ═══════════════════════════════════════════════
118
- # UI LAYOUT — Everything fits ONE screen (~500px)
119
- #
120
- # ┌────────────────────────────────────┐
121
- # │ 🔬 BERTopic Agent (compact) │ ~55px
122
- # ├────────────────────────────────────┤
123
- # │ [📂 Upload CSV] │ ~35px
124
- # ├────────────────────────────────────┤
125
- # │ 💬 Chat bubbles (cloud style) │
126
- # │ User: "Analyze my data" │ ~320px
127
- # │ Agent: "I found 1390 papers. │
128
- # │ Which config? All 3?" │
129
- # ├────────────────────────────────────┤
130
- # │ [Type message... ] [⏎] │ ~35px
131
- # ├────────────────────────────────────┤
132
- # │ 📥 Download │ ~35px
133
- # └────────────────────────────────────┘
134
- # TOTAL: ~480px — fits any screen
135
- # ═══════════════════════════════════════════════
136
- print(">>> Building UI...")
137
- with gr.Blocks(title="Topic Modelling — Agentic AI") as demo:
138
- gr.HTML(HEADER_HTML)
139
-
140
- # ══════════════════════════════════════════
141
- # SECTION 1: DATA INPUT
142
- # ══════════════════════════════════════════
143
- gr.HTML('<div class="section-label">① DATA INPUT</div>')
144
- with gr.Group():
145
- with gr.Row():
146
- upload = gr.File(label="📂 Upload Scopus CSV", file_types=[".csv"])
147
- gr.Markdown("**Upload your CSV** then type `run abstract only` in the chat below")
148
-
149
- # ══════════════════════════════════════════
150
- # SECTION 2: AGENT CONVERSATION
151
- # ══════════════════════════════════════════
152
- gr.HTML('<div class="section-label">② AGENT CONVERSATION — follow the prompts below</div>')
153
- with gr.Group():
154
- chatbot = gr.Chatbot(height=250, show_label=False,
155
- placeholder="Upload your Scopus CSV above, then type: run abstract only")
156
- with gr.Row():
157
- msg = gr.Textbox(
158
- placeholder="run · approve · show topic 4 papers · group 0 1 5 · done",
159
- show_label=False, scale=9, lines=1, max_lines=1, container=False)
160
- send = gr.Button("Send", variant="primary", scale=1, min_width=70)
161
-
162
- # ══════════════════════════════════════════
163
- # SECTION 3: RESULTS
164
- # ══════════════════════════════════════════
165
- gr.HTML('<div class="section-label">③ RESULTS — review table, charts, downloads</div>')
166
- with gr.Group():
167
- with gr.Tabs():
168
- with gr.Tab("📋 Review Table"):
169
- gr.Markdown("*Edit Approve / Rename To / Your Reasoning → click Submit. "
170
- "Type `show topic 4 papers` in chat to see paper list.*")
171
- review_table = gr.Dataframe(
172
- headers=["#", "Topic Label", "Research Area", "AI Reasoning",
173
- "Top Evidence Sentence", "Sentences", "Papers",
174
- "Approve", "Rename To", "Your Reasoning"],
175
- datatype=["number", "str", "str", "str", "str", "number", "number",
176
- "str", "str", "str"],
177
- column_count=(10, "fixed"),
178
- interactive=True,
179
- row_count=(1, "dynamic"),
180
- )
181
- submit_review = gr.Button("✅ Submit Review to Agent", variant="primary")
182
-
183
- # Paper viewer — select topic to see papers
184
- gr.Markdown("---")
185
- gr.Markdown("**📄 View papers in a topic:**")
186
- with gr.Row():
187
- topic_num = gr.Number(label="Topic #", value=0, precision=0, minimum=0, scale=1)
188
- view_papers_btn = gr.Button("Show Papers", scale=1)
189
- paper_list = gr.Textbox(label="Papers in selected topic", lines=8,
190
- interactive=False)
191
-
192
- with gr.Tab("📊 Charts"):
193
- chart_selector = gr.Dropdown(choices=[], label="Select Chart", interactive=True)
194
- chart_display = gr.HTML(
195
- value="<div style='height:350px;display:flex;align-items:center;justify-content:center;"
196
- "color:#94a3b8;border:1px dashed #cbd5e1;border-radius:8px;'>"
197
- "Charts appear after BERTopic runs</div>")
198
-
199
- with gr.Tab("📥 Download"):
200
- gr.Markdown("*All generated files — charts, labels, summaries, reports*")
201
- download = gr.File(label="Output files", file_count="multiple")
202
-
203
- def _load_chart(chart_name):
204
- """Load chart HTML into iframe — gr.HTML strips scripts, iframe executes them."""
205
- import html as html_mod
206
- path = f"/tmp/{chart_name}"
207
- content = os.path.exists(path) and open(path).read() or ""
208
- escaped = html_mod.escape(content) * bool(content)
209
- return (f'<iframe srcdoc="{escaped}" width="100%" height="450" '
210
- f'frameborder="0" sandbox="allow-scripts allow-same-origin"></iframe>'
211
- ) * bool(escaped) or "<div style='padding:20px;color:#94a3b8;'>Select a chart above</div>"
212
-
213
- def _get_chart_choices():
214
- """Find all rq4_*.html chart files in /tmp."""
215
- files = sorted(glob.glob("/tmp/rq4_*.html"))
216
- return list(map(os.path.basename, files))
217
-
218
- def _load_review_table():
219
- """Load review table from labels.json (if labeled) or summaries.json (if only discovered).
220
- Returns rows with: #, Label, Area, AI Reasoning, Top Evidence, Sent, Papers, Approve, Rename, Reasoning"""
221
- import json
222
- label_files = sorted(glob.glob("/tmp/checkpoints/rq4_*_labels.json"))
223
- summary_files = sorted(glob.glob("/tmp/checkpoints/rq4_*_summaries.json"))
224
- path = (label_files and label_files[-1]) or (summary_files and summary_files[-1]) or ""
225
- is_labeled = bool(label_files and label_files[-1] == path)
226
- data = (os.path.exists(path) and json.load(open(path))) or []
227
- rows = list(map(
228
- lambda l: [
229
- l.get("topic_id", 0),
230
- l.get("label", l.get("top_words", "")[:60]),
231
- l.get("category", "pending" * (not is_labeled)),
232
- l.get("reasoning", "pending labeling" * (not is_labeled)),
233
- (l.get("nearest", [{}])[0].get("sentence", "")[:120] + "...") * bool(l.get("nearest")),
234
- l.get("sentence_count", 0),
235
- l.get("paper_count", 0),
236
- "yes",
237
- "",
238
- "",
239
- ], data))
240
- return rows or [[0, "No data yet", "", "", "", 0, 0, "", "", ""]]
241
-
242
- def _show_papers(topic_id):
243
- """Show 5 nearest centroid sentences (evidence) + all paper titles for selected topic."""
244
- import json
245
- topic_id = int(topic_id)
246
- summary_files = sorted(glob.glob("/tmp/checkpoints/rq4_*_summaries.json"))
247
- label_files = sorted(glob.glob("/tmp/checkpoints/rq4_*_labels.json"))
248
- all_files = label_files or summary_files
249
- source_labels = list(map(
250
- lambda f: os.path.basename(f).split("_")[1], all_files))
251
- all_data = list(map(lambda f: json.load(open(f)), all_files))
252
-
253
- lines = []
254
- list(map(lambda pair: list(map(
255
- lambda t: (t.get("topic_id") == topic_id) and lines.append(
256
- f"═══ {pair[0].upper()} — Topic {topic_id}: "
257
- f"{t.get('label', t.get('top_words','')[:50])} ═══\n"
258
- f"{t.get('sentence_count', 0)} sentences from {t.get('paper_count', 0)} papers\n"
259
- f"AI Reasoning: {t.get('reasoning', 'not yet labeled')}\n\n"
260
- f"── 5 NEAREST CENTROID SENTENCES (evidence) ──\n"
261
- + "\n".join(list(map(
262
- lambda i: f" {i+1}. \"{t['nearest'][i]['sentence'][:200]}\"\n"
263
- f" Paper: {t['nearest'][i].get('title', '')[:100]}",
264
- range(min(5, len(t.get('nearest', [])))))))
265
- + "\n\n── ALL PAPER TITLES ──\n"
266
- + "\n".join(list(map(
267
- lambda i: f" {i+1}. {t['paper_titles'][i]}",
268
- range(len(t.get('paper_titles', []))))))
269
- ),
270
- pair[1])),
271
- zip(source_labels, all_data)))
272
-
273
- return "\n\n".join(lines) or f"Topic {topic_id} not found."
274
-
275
- view_papers_btn.click(_show_papers, [topic_num], [paper_list])
276
-
277
- def _submit_review(table_data, chat_history):
278
- """Convert edited review table into a message for the agent.
279
- Researcher's edits (approve/rename/reasoning) become natural language."""
280
- rows = table_data.values.tolist()
281
- lines = list(map(
282
- lambda r: (
283
- f"Topic {int(r[0])}: "
284
- + (f"RENAME to '{r[7]}'" * bool(str(r[7]).strip()))
285
- + (f"APPROVE '{r[1]}'" * (not bool(str(r[7]).strip())) * (str(r[6]).lower().startswith("y")))
286
- + (f"REJECT" * (str(r[6]).lower().startswith("n")))
287
- + (f" — reason: {r[8]}" * bool(str(r[8]).strip()))
288
- ), rows))
289
- review_msg = "Review decisions:\n" + "\n".join(lines)
290
- print(f">>> Review submitted: {review_msg[:200]}")
291
-
292
- chat_history = chat_history + [
293
- {"role": "user", "content": review_msg},
294
- {"role": "assistant", "content": "🔬 **Processing review decisions...**"},
295
- ]
296
- yield chat_history, _latest_output(), gr.update(), gr.update()
297
-
298
- result = agent.invoke(
299
- {"messages": [("human", review_msg)]},
300
- config={"configurable": {"thread_id": "session"}},
301
- )
302
- response = result["messages"][-1].content
303
- chat_history[-1] = {"role": "assistant", "content": response}
304
- yield chat_history, _latest_output(), gr.update(choices=_get_chart_choices()), gr.update()
305
-
306
- chart_selector.change(_load_chart, [chart_selector], [chart_display])
307
- submit_review.click(_submit_review, [review_table, chatbot],
308
- [chatbot, download, chart_selector, chart_display])
309
-
310
- def respond_with_viz(message, chat_history, uploaded_file):
311
- """Wrap respond() and update chart dropdown + review table after each message."""
312
- gen = respond(message, chat_history, uploaded_file)
313
- # First yield (progress)
314
- hist, txt, dl = next(gen)
315
- yield hist, txt, dl, gr.update(choices=_get_chart_choices()), gr.update(), gr.update()
316
- # Second yield (final response + populate table + charts)
317
- hist, txt, dl = next(gen)
318
- choices = _get_chart_choices()
319
- first_chart = (choices and _load_chart(choices[-1])) or gr.update()
320
- table_data = _load_review_table()
321
- yield (hist, txt, dl,
322
- gr.update(choices=choices, value=(choices and choices[-1]) or None),
323
- first_chart,
324
- gr.update(value=table_data))
325
-
326
- msg.submit(respond_with_viz, [msg, chatbot, upload],
327
- [chatbot, msg, download, chart_selector, chart_display, review_table])
328
- send.click(respond_with_viz, [msg, chatbot, upload],
329
- [chatbot, msg, download, chart_selector, chart_display, review_table])
330
-
331
- def _auto_load_csv(uploaded_file, chat_history):
332
- """Auto-trigger analysis when CSV is uploaded — stats appear without typing."""
333
- gen = respond("Analyze my Scopus CSV", chat_history, uploaded_file)
334
- hist, txt, dl = next(gen)
335
- yield hist, dl, gr.update(), gr.update(), gr.update()
336
- hist, txt, dl = next(gen)
337
- choices = _get_chart_choices()
338
- first_chart = (choices and _load_chart(choices[-1])) or gr.update()
339
- table_data = _load_review_table()
340
- yield (hist, dl,
341
- gr.update(choices=choices, value=(choices and choices[-1]) or None),
342
- first_chart,
343
- gr.update(value=table_data))
344
-
345
- upload.change(_auto_load_csv, [upload, chatbot],
346
- [chatbot, download, chart_selector, chart_display, review_table])
347
-
348
- print(">>> Launching...")
349
- demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)