Shivani-Bhat commited on
Commit
73c3be7
·
verified ·
1 Parent(s): 366c743

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +110 -58
app.py CHANGED
@@ -30,14 +30,13 @@ from typing import Optional, Tuple, Dict, Any
30
  from agent import PAJAISResearchAgent, AnalysisConfig
31
  from tools import (
32
  load_journal_csv, validate_dataframe,
33
- PAJAIS_THEMES, export_all_artifacts
34
- )
35
- from tools_additions import (
36
- dbscan_cluster_topics,
37
- enforce_min_membership,
38
- split_large_clusters,
39
  get_cluster_summary,
40
- label_clusters_with_llm,
41
  run_agentic_council,
42
  )
43
 
@@ -57,6 +56,7 @@ OUTPUTS_DIR.mkdir(exist_ok=True)
57
  # ---------------------------------------------------------------------------
58
  MISTRAL_API_KEY = os.environ.get("MISTRAL_API_KEY", "")
59
  GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
 
60
 
61
  # ---------------------------------------------------------------------------
62
  # Custom CSS — Light, readable theme that works on HuggingFace Spaces
@@ -1461,40 +1461,57 @@ with gr.Blocks(
1461
  # ==================================================================
1462
  # TAB A — DBSCAN Clusters (Phase 2.5)
1463
  # ==================================================================
1464
- with gr.Tab("🔵 DBSCAN Clusters"):
1465
- gr.Markdown("## Phase 2.5: Semantic Clustering via DBSCAN")
1466
  gr.Markdown(
1467
- "Papers are embedded separately as **title vectors** and **abstract vectors** "
1468
- "(TF-IDF LSA), clustered independently with DBSCAN, then merged via weighted vote. "
1469
- "Large clusters are recursively split; tiny clusters are reassigned or marked noise."
 
 
 
 
 
1470
  )
1471
 
1472
  with gr.Accordion("⚙️ Clustering Parameters", open=False):
1473
  with gr.Row():
1474
- eps_title_slider = gr.Slider(
1475
- 0.05, 0.60, value=0.25, step=0.01, label="ε Title (cosine distance threshold)"
 
 
1476
  )
1477
- eps_abstract_slider = gr.Slider(
1478
- 0.05, 0.60, value=0.30, step=0.01, label="ε Abstract"
 
 
1479
  )
1480
  with gr.Row():
1481
- min_samples_slider = gr.Slider(
1482
- 2, 20, value=2, step=1, label="Min Samples (DBSCAN core-point threshold)"
 
 
1483
  )
1484
- min_members_slider = gr.Slider(
1485
- 2, 20, value=3, step=1, label="Min Cluster Membership (post-processing)"
 
 
1486
  )
1487
  with gr.Row():
1488
- max_size_slider = gr.Slider(
1489
- 10, 200, value=30, step=5, label="Max Cluster Size (triggers splitting)"
 
 
1490
  )
1491
- vote_weight_slider = gr.Slider(
1492
- 0.0, 1.0, value=0.6, step=0.05, label="Abstract Vote Weight (vs Title)"
 
 
1493
  )
1494
 
1495
  with gr.Row():
1496
- btn_run_dbscan = gr.Button("▶ Run DBSCAN Clustering", variant="primary")
1497
- btn_llm_label = gr.Button("🤖 Label Clusters with LLM", variant="secondary")
1498
 
1499
  dbscan_status = gr.Markdown("*Run DBSCAN or use the full pipeline from Tab 1.*")
1500
 
@@ -1527,7 +1544,7 @@ with gr.Blocks(
1527
 
1528
  def handle_run_dbscan(
1529
  df, existing_cluster_df, existing_summary,
1530
- eps_t, eps_a, min_s, min_m, max_sz, vote_w,
1531
  progress=gr.Progress(track_tqdm=True)
1532
  ):
1533
  if existing_cluster_df is not None and not existing_cluster_df.empty:
@@ -1536,7 +1553,7 @@ with gr.Blocks(
1536
  saved_docs = _safe_save_csv(existing_cluster_df, "cluster_documents.csv")
1537
  saved_sum = _safe_save_csv(summary, "cluster_summary.csv")
1538
  return (
1539
- "<div class='success-box'>✅ Loaded existing DBSCAN results.</div>",
1540
  summary, existing_cluster_df, summary,
1541
  fig_sz, fig_noise,
1542
  gr.update(value=saved_docs), gr.update(value=saved_sum), gr.update(),
@@ -1552,19 +1569,26 @@ with gr.Blocks(
1552
 
1553
  try:
1554
  _ensure_output_dir()
1555
- progress(0.1, desc="Vectorising documents…")
1556
- cdf = dbscan_cluster_topics(
1557
- df,
1558
- eps_title=eps_t, eps_abstract=eps_a,
1559
- min_samples=int(min_s),
1560
- n_svd_components=64,
1561
- vote_weight_abstract=vote_w,
 
 
 
 
 
 
 
 
 
 
 
1562
  )
1563
- progress(0.5, desc="Enforcing min membership…")
1564
- cdf = enforce_min_membership(cdf, min_members=int(min_m))
1565
- progress(0.7, desc="Splitting large clusters…")
1566
- cdf = split_large_clusters(cdf, max_cluster_size=int(max_sz))
1567
- progress(0.9, desc="Summarising…")
1568
  summary = get_cluster_summary(cdf)
1569
  progress(1.0, desc="Done!")
1570
 
@@ -1572,8 +1596,8 @@ with gr.Blocks(
1572
  saved_docs = _safe_save_csv(cdf, "cluster_documents.csv")
1573
  saved_sum = _safe_save_csv(summary, "cluster_summary.csv")
1574
 
1575
- n_c = len(set(cdf["cluster_final"]) - {-1})
1576
- n_n = int(cdf["is_noise"].sum())
1577
  return (
1578
  f"<div class='success-box'>✅ {n_c} clusters found, {n_n} noise docs.</div>",
1579
  summary, cdf, summary,
@@ -1591,21 +1615,36 @@ with gr.Blocks(
1591
  def handle_llm_label(cluster_df, cluster_summary, progress=gr.Progress(track_tqdm=True)):
1592
  if cluster_df is None or cluster_df.empty:
1593
  return (
1594
- "<div class='error-box'>❌ Run DBSCAN first.</div>",
1595
  cluster_summary, gr.update()
1596
  )
1597
  try:
1598
  _ensure_output_dir()
1599
- progress(0.2, desc="Sending clusters to LLM…")
1600
- labeled = label_clusters_with_llm(
 
 
 
 
 
 
 
 
 
 
1601
  cluster_df=cluster_df,
1602
- cluster_summary_df=cluster_summary.copy() if cluster_summary is not None else get_cluster_summary(cluster_df),
1603
- max_clusters=50,
 
 
 
 
 
1604
  )
1605
  progress(1.0, desc="Done!")
1606
  saved = _safe_save_csv(labeled, "cluster_labels.csv")
1607
  return (
1608
- "<div class='success-box'>✅ Clusters labeled by LLM.</div>",
1609
  labeled,
1610
  gr.update(value=saved),
1611
  )
@@ -1619,9 +1658,9 @@ with gr.Blocks(
1619
  fn=handle_run_dbscan,
1620
  inputs=[
1621
  state_df, state_cluster_df, state_cluster_summary,
1622
- eps_title_slider, eps_abstract_slider,
1623
- min_samples_slider, min_members_slider,
1624
- max_size_slider, vote_weight_slider,
1625
  ],
1626
  outputs=[
1627
  dbscan_status,
@@ -1653,9 +1692,10 @@ with gr.Blocks(
1653
  with gr.Tab("🧠 Agentic Council"):
1654
  gr.Markdown("## Phase 6.5: Dual-Model Research Council")
1655
  gr.Markdown(
1656
- "Two AI models independently assess the PAJAIS research gap findings:\n"
1657
  "- **Mistral** (Panel A) — pragmatic applied IS perspective\n"
1658
- "- **Gemini** (Panel B) — broad technology futures perspective\n\n"
 
1659
  "API keys are loaded automatically from HuggingFace Secrets "
1660
  "(`MISTRAL_API_KEY`, `GEMINI_API_KEY`). "
1661
  "Configure them in your Space under **Settings → Variables and Secrets**."
@@ -1664,8 +1704,9 @@ with gr.Blocks(
1664
  # Key-status indicator — shows which secrets are present at load time
1665
  _key_lines = ["**🔑 Secret Status (loaded at startup):**"]
1666
  for _label, _val in [
1667
- ("MISTRAL_API_KEY", MISTRAL_API_KEY),
1668
- ("GEMINI_API_KEY", GEMINI_API_KEY),
 
1669
  ]:
1670
  _icon = "✅ present" if _val else "❌ missing"
1671
  _key_lines.append(f"- `{_label}`: {_icon}")
@@ -1691,6 +1732,14 @@ with gr.Blocks(
1691
  interactive=False,
1692
  show_label=True,
1693
  )
 
 
 
 
 
 
 
 
1694
 
1695
  dl_council = gr.DownloadButton("⬇ council_report.json", value=None)
1696
 
@@ -1721,6 +1770,7 @@ with gr.Blocks(
1721
  topic_df=topic_df,
1722
  mistral_api_key=MISTRAL_API_KEY,
1723
  gemini_api_key=GEMINI_API_KEY,
 
1724
  )
1725
  progress(0.9, desc="Saving report…")
1726
  saved = _safe_save_json(result, "council_report.json")
@@ -1731,18 +1781,19 @@ with gr.Blocks(
1731
  status,
1732
  result.get("mistral", ""),
1733
  result.get("gemini", ""),
 
1734
  gr.update(value=saved),
1735
  )
1736
  except Exception as e:
1737
  return (
1738
  f"<div class='error-box'>❌ Council failed: {e}</div>",
1739
- "", "", gr.update()
1740
  )
1741
 
1742
  btn_run_council.click(
1743
  fn=handle_run_council,
1744
  inputs=[state_taxonomy_map, state_topic_df],
1745
- outputs=[council_status, mistral_output, gemini_output, dl_council]
1746
  )
1747
 
1748
  # Auto-fill if council already ran (e.g. via full pipeline)
@@ -1750,9 +1801,10 @@ with gr.Blocks(
1750
  fn=lambda cr: (
1751
  cr.get("mistral", "") if cr else "",
1752
  cr.get("gemini", "") if cr else "",
 
1753
  ),
1754
  inputs=[state_council_result],
1755
- outputs=[mistral_output, gemini_output]
1756
  )
1757
 
1758
  # ==================================================================
 
30
  from agent import PAJAISResearchAgent, AnalysisConfig
31
  from tools import (
32
  load_journal_csv, validate_dataframe,
33
+ PAJAIS_THEMES, export_all_artifacts,
34
+ # Unified clustering pipeline (all now in tools.py)
35
+ build_title_abstract_column,
36
+ embed_with_specter2,
37
+ specter2_hdbscan_cluster_topics,
 
38
  get_cluster_summary,
39
+ label_clusters_3llm,
40
  run_agentic_council,
41
  )
42
 
 
56
  # ---------------------------------------------------------------------------
57
  MISTRAL_API_KEY = os.environ.get("MISTRAL_API_KEY", "")
58
  GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
59
+ DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
60
 
61
  # ---------------------------------------------------------------------------
62
  # Custom CSS — Light, readable theme that works on HuggingFace Spaces
 
1461
  # ==================================================================
1462
  # TAB A — DBSCAN Clusters (Phase 2.5)
1463
  # ==================================================================
1464
+ with gr.Tab("🔵 SPECTER2 Clusters"):
1465
+ gr.Markdown("## Phase 2.5: Semantic Clustering via SPECTER2 → UMAP → HDBSCAN")
1466
  gr.Markdown(
1467
+ "Each paper is represented by **one 768-dim SPECTER2 vector** computed from its "
1468
+ "combined Title + Abstract column (DOI-keyed). "
1469
+ "UMAP reduces dimensions (cosine metric, 50D), then HDBSCAN clusters with an "
1470
+ "automatic parameter sweep to land in the **15–30 cluster** target range. "
1471
+ "Clusters with fewer than 5 or more than 100 papers are automatically merged/split. "
1472
+ "Intra-cluster cosine similarity is kept in the **0.50–0.60** band. "
1473
+ "The 3 most representative paper titles per cluster are sent to "
1474
+ "**Mistral + Gemini + HuggingFace** (all free) for labeling — majority vote wins."
1475
  )
1476
 
1477
  with gr.Accordion("⚙️ Clustering Parameters", open=False):
1478
  with gr.Row():
1479
+ min_cs_slider = gr.Slider(
1480
+ 2, 20, value=5, step=1,
1481
+ label="Min Cluster Size (papers)",
1482
+ info="Papers < this → merged into nearest cluster"
1483
  )
1484
+ max_cs_slider = gr.Slider(
1485
+ 20, 200, value=100, step=5,
1486
+ label="Max Cluster Size (papers)",
1487
+ info="Papers > this → cluster is split"
1488
  )
1489
  with gr.Row():
1490
+ target_min_slider = gr.Slider(
1491
+ 5, 20, value=15, step=1,
1492
+ label="Target Min Clusters",
1493
+ info="HDBSCAN sweep lower bound"
1494
  )
1495
+ target_max_slider = gr.Slider(
1496
+ 15, 40, value=30, step=1,
1497
+ label="Target Max Clusters",
1498
+ info="HDBSCAN sweep upper bound"
1499
  )
1500
  with gr.Row():
1501
+ sim_low_slider = gr.Slider(
1502
+ 0.30, 0.70, value=0.50, step=0.01,
1503
+ label="Min Cosine Similarity (cluster quality)",
1504
+ info="Clusters below this are dissolved to noise"
1505
  )
1506
+ umap_neighbors_slider = gr.Slider(
1507
+ 5, 50, value=15, step=1,
1508
+ label="UMAP n_neighbors",
1509
+ info="Controls local vs global structure"
1510
  )
1511
 
1512
  with gr.Row():
1513
+ btn_run_dbscan = gr.Button("▶ Run SPECTER2 → UMAP → HDBSCAN", variant="primary")
1514
+ btn_llm_label = gr.Button("🤖 Label Clusters (3 LLMs)", variant="secondary")
1515
 
1516
  dbscan_status = gr.Markdown("*Run DBSCAN or use the full pipeline from Tab 1.*")
1517
 
 
1544
 
1545
  def handle_run_dbscan(
1546
  df, existing_cluster_df, existing_summary,
1547
+ min_cs, max_cs, target_min, target_max, sim_low, umap_n,
1548
  progress=gr.Progress(track_tqdm=True)
1549
  ):
1550
  if existing_cluster_df is not None and not existing_cluster_df.empty:
 
1553
  saved_docs = _safe_save_csv(existing_cluster_df, "cluster_documents.csv")
1554
  saved_sum = _safe_save_csv(summary, "cluster_summary.csv")
1555
  return (
1556
+ "<div class='success-box'>✅ Loaded existing results.</div>",
1557
  summary, existing_cluster_df, summary,
1558
  fig_sz, fig_noise,
1559
  gr.update(value=saved_docs), gr.update(value=saved_sum), gr.update(),
 
1569
 
1570
  try:
1571
  _ensure_output_dir()
1572
+ progress(0.05, desc="Building title+abstract column…")
1573
+ df_ta = build_title_abstract_column(df)
1574
+
1575
+ progress(0.15, desc="Generating SPECTER2 embeddings (may take 2-5 min)…")
1576
+ texts = df_ta['title_abstract'].tolist()
1577
+ embs = embed_with_specter2(texts, cache_dir='outputs/specter_cache')
1578
+
1579
+ progress(0.60, desc="UMAP + HDBSCAN clustering…")
1580
+ cdf = specter2_hdbscan_cluster_topics(
1581
+ df=df_ta,
1582
+ embeddings=embs,
1583
+ min_cluster_size=int(min_cs),
1584
+ max_cluster_size=int(max_cs),
1585
+ target_min_clusters=int(target_min),
1586
+ target_max_clusters=int(target_max),
1587
+ cosine_sim_low=float(sim_low),
1588
+ cosine_sim_high=float(sim_low) + 0.10,
1589
+ umap_n_neighbors=int(umap_n),
1590
  )
1591
+ progress(0.85, desc="Summarising clusters…")
 
 
 
 
1592
  summary = get_cluster_summary(cdf)
1593
  progress(1.0, desc="Done!")
1594
 
 
1596
  saved_docs = _safe_save_csv(cdf, "cluster_documents.csv")
1597
  saved_sum = _safe_save_csv(summary, "cluster_summary.csv")
1598
 
1599
+ n_c = len(set(cdf['cluster_final']) - {-1})
1600
+ n_n = int(cdf['is_noise'].sum())
1601
  return (
1602
  f"<div class='success-box'>✅ {n_c} clusters found, {n_n} noise docs.</div>",
1603
  summary, cdf, summary,
 
1615
  def handle_llm_label(cluster_df, cluster_summary, progress=gr.Progress(track_tqdm=True)):
1616
  if cluster_df is None or cluster_df.empty:
1617
  return (
1618
+ "<div class='error-box'>❌ Run clustering first.</div>",
1619
  cluster_summary, gr.update()
1620
  )
1621
  try:
1622
  _ensure_output_dir()
1623
+ # Load cached embeddings if available
1624
+ import glob
1625
+ cache_files = glob.glob('outputs/specter_cache/*.npy')
1626
+ if not cache_files:
1627
+ return (
1628
+ "<div class='error-box'>❌ No SPECTER2 cache found. Run clustering tab first.</div>",
1629
+ cluster_summary, gr.update()
1630
+ )
1631
+ embs = np.load(sorted(cache_files)[-1]) # most recent cache
1632
+
1633
+ progress(0.2, desc="Sending clusters to LLMs…")
1634
+ labeled = label_clusters_3llm(
1635
  cluster_df=cluster_df,
1636
+ cluster_summary_df=cluster_summary.copy() if cluster_summary is not None
1637
+ else get_cluster_summary(cluster_df),
1638
+ embeddings=embs,
1639
+ mistral_api_key=MISTRAL_API_KEY,
1640
+ gemini_api_key=GEMINI_API_KEY,
1641
+ deepseek_api_key=DEEPSEEK_API_KEY,
1642
+ max_clusters=30,
1643
  )
1644
  progress(1.0, desc="Done!")
1645
  saved = _safe_save_csv(labeled, "cluster_labels.csv")
1646
  return (
1647
+ "<div class='success-box'>✅ Clusters labeled by 3 LLMs (majority vote).</div>",
1648
  labeled,
1649
  gr.update(value=saved),
1650
  )
 
1658
  fn=handle_run_dbscan,
1659
  inputs=[
1660
  state_df, state_cluster_df, state_cluster_summary,
1661
+ min_cs_slider, max_cs_slider,
1662
+ target_min_slider, target_max_slider,
1663
+ sim_low_slider, umap_neighbors_slider,
1664
  ],
1665
  outputs=[
1666
  dbscan_status,
 
1692
  with gr.Tab("🧠 Agentic Council"):
1693
  gr.Markdown("## Phase 6.5: Dual-Model Research Council")
1694
  gr.Markdown(
1695
+ "Three AI models independently assess the PAJAIS research gap findings:\n"
1696
  "- **Mistral** (Panel A) — pragmatic applied IS perspective\n"
1697
+ "- **Gemini** (Panel B) — broad technology futures perspective\n"
1698
+ "- **DeepSeek** (Panel C) — deep analytical synthesis\n\n"
1699
  "API keys are loaded automatically from HuggingFace Secrets "
1700
  "(`MISTRAL_API_KEY`, `GEMINI_API_KEY`). "
1701
  "Configure them in your Space under **Settings → Variables and Secrets**."
 
1704
  # Key-status indicator — shows which secrets are present at load time
1705
  _key_lines = ["**🔑 Secret Status (loaded at startup):**"]
1706
  for _label, _val in [
1707
+ ("MISTRAL_API_KEY", MISTRAL_API_KEY),
1708
+ ("GEMINI_API_KEY", GEMINI_API_KEY),
1709
+ ("DEEPSEEK_API_KEY", DEEPSEEK_API_KEY),
1710
  ]:
1711
  _icon = "✅ present" if _val else "❌ missing"
1712
  _key_lines.append(f"- `{_label}`: {_icon}")
 
1732
  interactive=False,
1733
  show_label=True,
1734
  )
1735
+ with gr.Column():
1736
+ gr.Markdown("### 🟣 Panel C — DeepSeek")
1737
+ deepseek_output = gr.Textbox(
1738
+ label="DeepSeek Assessment",
1739
+ lines=18,
1740
+ interactive=False,
1741
+ show_label=True,
1742
+ )
1743
 
1744
  dl_council = gr.DownloadButton("⬇ council_report.json", value=None)
1745
 
 
1770
  topic_df=topic_df,
1771
  mistral_api_key=MISTRAL_API_KEY,
1772
  gemini_api_key=GEMINI_API_KEY,
1773
+ deepseek_api_key=DEEPSEEK_API_KEY,
1774
  )
1775
  progress(0.9, desc="Saving report…")
1776
  saved = _safe_save_json(result, "council_report.json")
 
1781
  status,
1782
  result.get("mistral", ""),
1783
  result.get("gemini", ""),
1784
+ result.get("deepseek", ""),
1785
  gr.update(value=saved),
1786
  )
1787
  except Exception as e:
1788
  return (
1789
  f"<div class='error-box'>❌ Council failed: {e}</div>",
1790
+ "", "", "", gr.update()
1791
  )
1792
 
1793
  btn_run_council.click(
1794
  fn=handle_run_council,
1795
  inputs=[state_taxonomy_map, state_topic_df],
1796
+ outputs=[council_status, mistral_output, gemini_output, deepseek_output, dl_council]
1797
  )
1798
 
1799
  # Auto-fill if council already ran (e.g. via full pipeline)
 
1801
  fn=lambda cr: (
1802
  cr.get("mistral", "") if cr else "",
1803
  cr.get("gemini", "") if cr else "",
1804
+ cr.get("deepseek", "") if cr else "",
1805
  ),
1806
  inputs=[state_council_result],
1807
+ outputs=[mistral_output, gemini_output, deepseek_output]
1808
  )
1809
 
1810
  # ==================================================================