Spaces:
Sleeping
Sleeping
Delete app.py
Browse files
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|