Update app.py
Browse files
app.py
CHANGED
|
@@ -75,6 +75,7 @@ def retry(max_attempts=3, delay=1):
|
|
| 75 |
# LAB JOURNAL
|
| 76 |
# ─────────────────────────────────────────────
|
| 77 |
JOURNAL_FILE = "./lab_journal.csv"
|
|
|
|
| 78 |
JOURNAL_CATEGORIES = [
|
| 79 |
# Real Data Tools (S2-A)
|
| 80 |
"S2-A·R1a", # Gray Zones Explorer
|
|
@@ -82,7 +83,7 @@ JOURNAL_CATEGORIES = [
|
|
| 82 |
"S2-A·R1c", # Real Variant Lookup
|
| 83 |
"S2-A·R1d", # Literature Gap Finder
|
| 84 |
"S2-A·R1e", # Druggable Orphans
|
| 85 |
-
"S2-A·R1f", # Research Assistant
|
| 86 |
# Learning Sandbox (S2-B)
|
| 87 |
"S2-B·R1a", # miRNA Explorer
|
| 88 |
"S2-B·R1b", # siRNA Targets
|
|
@@ -117,12 +118,22 @@ def journal_read(category: str = "All") -> str:
|
|
| 117 |
if df.empty:
|
| 118 |
return f"No entries for category: {category}"
|
| 119 |
# Format for better readability
|
| 120 |
-
df_display = df[["timestamp", "category", "action", "result_summary"]].tail(
|
| 121 |
return df_display.to_markdown(index=False)
|
| 122 |
except Exception as e:
|
| 123 |
print(f"Journal read error: {e}")
|
| 124 |
return "Error reading journal."
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
# ─────────────────────────────────────────────
|
| 127 |
# CONSTANTS
|
| 128 |
# ─────────────────────────────────────────────
|
|
@@ -258,6 +269,7 @@ def ot_query(gql: str, variables: dict = None) -> dict:
|
|
| 258 |
except Exception as e:
|
| 259 |
print(f"OT query error: {e}")
|
| 260 |
return {"error": str(e)}
|
|
|
|
| 261 |
# ─────────────────────────────────────────────
|
| 262 |
# TAB A1 — GRAY ZONES EXPLORER
|
| 263 |
# ─────────────────────────────────────────────
|
|
@@ -280,7 +292,6 @@ def a1_run(cancer_type: str):
|
|
| 280 |
|
| 281 |
fig, ax = plt.subplots(figsize=(6, 8), facecolor="white")
|
| 282 |
valid = df[cancer_type].fillna(0).values.reshape(-1, 1)
|
| 283 |
-
# Виправлено deprecated get_cmap
|
| 284 |
cmap = plt.colormaps.get_cmap("YlOrRd")
|
| 285 |
cmap.set_bad("white")
|
| 286 |
masked = np.ma.masked_where(df[cancer_type].isna().values.reshape(-1, 1), valid)
|
|
@@ -313,7 +324,7 @@ def a1_run(cancer_type: str):
|
|
| 313 |
)
|
| 314 |
|
| 315 |
gaps_md = "\n\n---\n\n".join(gap_cards) if gap_cards else "No data available."
|
| 316 |
-
journal_log("
|
| 317 |
source_note = f"*Source: PubMed E-utilities | Date: {today}*"
|
| 318 |
return img, gaps_md + "\n\n" + source_note
|
| 319 |
|
|
@@ -447,9 +458,9 @@ def a2_run(cancer_type: str):
|
|
| 447 |
f"and replace `_load_depmap_sample()` in `app.py`."
|
| 448 |
)
|
| 449 |
if not result_df.empty:
|
| 450 |
-
journal_log("
|
| 451 |
else:
|
| 452 |
-
journal_log("
|
| 453 |
return result_df, note
|
| 454 |
|
| 455 |
|
|
@@ -575,7 +586,7 @@ def a3_run(hgvs: str):
|
|
| 575 |
)
|
| 576 |
|
| 577 |
result_parts.append(f"\n*Source: ClinVar E-utilities + gnomAD GraphQL | Date: {today}*")
|
| 578 |
-
journal_log("
|
| 579 |
return "\n\n".join(result_parts)
|
| 580 |
|
| 581 |
|
|
@@ -641,7 +652,7 @@ def a4_run(cancer_type: str, keyword: str):
|
|
| 641 |
|
| 642 |
summary = "\n\n".join(gap_text)
|
| 643 |
summary += f"\n\n*Source: PubMed E-utilities | Date: {today}*"
|
| 644 |
-
journal_log("
|
| 645 |
return img, summary
|
| 646 |
|
| 647 |
|
|
@@ -738,8 +749,10 @@ def a5_run(cancer_type: str):
|
|
| 738 |
f"*Source: OpenTargets GraphQL + ClinicalTrials.gov v2 | Date: {today}*\n\n"
|
| 739 |
f"*Orphan = no approved drug (OpenTargets knownDrugs.count = 0)*"
|
| 740 |
)
|
| 741 |
-
journal_log("
|
| 742 |
return df, note
|
|
|
|
|
|
|
| 743 |
# ─────────────────────────────────────────────
|
| 744 |
# GROUP B — LEARNING SANDBOX
|
| 745 |
# ─────────────────────────────────────────────
|
|
@@ -819,7 +832,7 @@ def b1_run(gene: str):
|
|
| 819 |
"Expression log2FC": changes,
|
| 820 |
})
|
| 821 |
context = f"\n\n**Cancer Context:** {db['cancer_context']}"
|
| 822 |
-
journal_log("
|
| 823 |
return img, df.to_markdown(index=False) + context
|
| 824 |
|
| 825 |
|
|
@@ -881,7 +894,7 @@ def b2_run(cancer: str):
|
|
| 881 |
"Off-target Risk": off_risk,
|
| 882 |
"Delivery Challenge": delivery,
|
| 883 |
})
|
| 884 |
-
journal_log("
|
| 885 |
return img, df.to_markdown(index=False)
|
| 886 |
|
| 887 |
|
|
@@ -937,7 +950,7 @@ def b3_run(peg_mol_pct: float, ionizable_pct: float, helper_pct: float,
|
|
| 937 |
+ ("High ApoE → enhanced brain/liver targeting via LDLR pathway." if apoe_pct > 25
|
| 938 |
else "Low ApoE → reduced receptor-mediated uptake.")
|
| 939 |
)
|
| 940 |
-
journal_log("
|
| 941 |
return img, interpretation
|
| 942 |
|
| 943 |
|
|
@@ -981,7 +994,7 @@ def b4_run(time_points: int, kon_albumin: float, kon_apoe: float,
|
|
| 981 |
"The Vroman effect describes sequential protein displacement: "
|
| 982 |
"abundant proteins (albumin) adsorb first, then are displaced by higher-affinity proteins (ApoE, fibrinogen)."
|
| 983 |
)
|
| 984 |
-
journal_log("
|
| 985 |
return img, note
|
| 986 |
|
| 987 |
|
|
@@ -1035,10 +1048,8 @@ def b5_run(classification: str):
|
|
| 1035 |
f"> ⚠️ SIMULATED — This is a rule-based educational model only. "
|
| 1036 |
f"Real variant classification requires expert review and full ACMG/AMP criteria evaluation."
|
| 1037 |
)
|
| 1038 |
-
journal_log("
|
| 1039 |
return output
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
# ─────────────────────────────────────────────
|
| 1043 |
# GRADIO UI ASSEMBLY
|
| 1044 |
# ─────────────────────────────────────────────
|
|
@@ -1055,17 +1066,16 @@ body { font-family: 'Inter', sans-serif; }
|
|
| 1055 |
background: #f8f9fa; border-left: 4px solid #d73027;
|
| 1056 |
padding: 10px 14px; margin: 6px 0; border-radius: 4px;
|
| 1057 |
}
|
| 1058 |
-
footer { display: none !important; }
|
| 1059 |
-
|
| 1060 |
/* Зміна курсора на pointer для всіх інтерактивних елементів */
|
| 1061 |
.gr-dropdown, .gr-button, .gr-slider, .gr-radio, .gr-checkbox,
|
| 1062 |
-
.tab-nav button, .gr-accordion, .gr-
|
| 1063 |
cursor: pointer !important;
|
| 1064 |
}
|
| 1065 |
/* Для текстових полів залишаємо звичайний текстовий курсор */
|
| 1066 |
.gr-textbox input, .gr-textarea textarea {
|
| 1067 |
cursor: text !important;
|
| 1068 |
}
|
|
|
|
| 1069 |
"""
|
| 1070 |
|
| 1071 |
|
|
@@ -1080,7 +1090,7 @@ def build_app():
|
|
| 1080 |
with gr.Row():
|
| 1081 |
# ── MAIN CONTENT ──
|
| 1082 |
with gr.Column(scale=4):
|
| 1083 |
-
with gr.Tabs():
|
| 1084 |
|
| 1085 |
# ════════════════════════════════
|
| 1086 |
# GROUP A — REAL DATA TOOLS
|
|
@@ -1361,9 +1371,33 @@ def build_app():
|
|
| 1361 |
)
|
| 1362 |
b5_btn.click(b5_run, inputs=[b5_class], outputs=[b5_result])
|
| 1363 |
|
| 1364 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1365 |
with gr.Column(scale=1, min_width=260):
|
| 1366 |
-
gr.Markdown("##
|
| 1367 |
note_category = gr.Dropdown(
|
| 1368 |
choices=JOURNAL_CATEGORIES,
|
| 1369 |
value="Manual",
|
|
@@ -1379,33 +1413,18 @@ def build_app():
|
|
| 1379 |
clear_note_btn = gr.Button("🗑️ Clear", size="sm")
|
| 1380 |
save_status = gr.Markdown("")
|
| 1381 |
|
| 1382 |
-
gr.Markdown("---")
|
| 1383 |
-
gr.Markdown("## 🔍 Full Journal")
|
| 1384 |
-
journal_filter = gr.Dropdown(
|
| 1385 |
-
choices=["All"] + JOURNAL_CATEGORIES,
|
| 1386 |
-
value="All",
|
| 1387 |
-
label="Filter by category"
|
| 1388 |
-
)
|
| 1389 |
-
refresh_btn = gr.Button("🔄 Refresh", size="sm")
|
| 1390 |
-
journal_display = gr.Markdown(value=journal_read())
|
| 1391 |
-
|
| 1392 |
def save_note(category, note):
|
| 1393 |
if note.strip():
|
| 1394 |
journal_log(category, "manual note", note, note)
|
| 1395 |
return "✅ Note saved.", ""
|
| 1396 |
return "⚠️ Note is empty.", ""
|
| 1397 |
|
| 1398 |
-
def refresh_journal(category):
|
| 1399 |
-
return journal_read(category)
|
| 1400 |
-
|
| 1401 |
save_btn.click(
|
| 1402 |
save_note,
|
| 1403 |
inputs=[note_category, note_input],
|
| 1404 |
outputs=[save_status, note_input]
|
| 1405 |
)
|
| 1406 |
clear_note_btn.click(lambda: ("", ""), outputs=[note_input, save_status])
|
| 1407 |
-
refresh_btn.click(refresh_journal, inputs=[journal_filter], outputs=journal_display)
|
| 1408 |
-
journal_filter.change(refresh_journal, inputs=[journal_filter], outputs=journal_display)
|
| 1409 |
|
| 1410 |
# === Інтеграція з Learning Playground ===
|
| 1411 |
with gr.Row():
|
|
|
|
| 75 |
# LAB JOURNAL
|
| 76 |
# ─────────────────────────────────────────────
|
| 77 |
JOURNAL_FILE = "./lab_journal.csv"
|
| 78 |
+
# Уніфікована система кодування (як у Learning Playground)
|
| 79 |
JOURNAL_CATEGORIES = [
|
| 80 |
# Real Data Tools (S2-A)
|
| 81 |
"S2-A·R1a", # Gray Zones Explorer
|
|
|
|
| 83 |
"S2-A·R1c", # Real Variant Lookup
|
| 84 |
"S2-A·R1d", # Literature Gap Finder
|
| 85 |
"S2-A·R1e", # Druggable Orphans
|
| 86 |
+
"S2-A·R1f", # Research Assistant (Chatbot)
|
| 87 |
# Learning Sandbox (S2-B)
|
| 88 |
"S2-B·R1a", # miRNA Explorer
|
| 89 |
"S2-B·R1b", # siRNA Targets
|
|
|
|
| 118 |
if df.empty:
|
| 119 |
return f"No entries for category: {category}"
|
| 120 |
# Format for better readability
|
| 121 |
+
df_display = df[["timestamp", "category", "action", "result_summary"]].tail(50)
|
| 122 |
return df_display.to_markdown(index=False)
|
| 123 |
except Exception as e:
|
| 124 |
print(f"Journal read error: {e}")
|
| 125 |
return "Error reading journal."
|
| 126 |
|
| 127 |
+
def clear_journal():
|
| 128 |
+
"""Delete the journal file."""
|
| 129 |
+
try:
|
| 130 |
+
if os.path.exists(JOURNAL_FILE):
|
| 131 |
+
os.remove(JOURNAL_FILE)
|
| 132 |
+
return "Journal cleared."
|
| 133 |
+
except Exception as e:
|
| 134 |
+
print(f"Clear journal error: {e}")
|
| 135 |
+
return "Error clearing journal."
|
| 136 |
+
|
| 137 |
# ─────────────────────────────────────────────
|
| 138 |
# CONSTANTS
|
| 139 |
# ─────────────────────────────────────────────
|
|
|
|
| 269 |
except Exception as e:
|
| 270 |
print(f"OT query error: {e}")
|
| 271 |
return {"error": str(e)}
|
| 272 |
+
|
| 273 |
# ─────────────────────────────────────────────
|
| 274 |
# TAB A1 — GRAY ZONES EXPLORER
|
| 275 |
# ─────────────────────────────────────────────
|
|
|
|
| 292 |
|
| 293 |
fig, ax = plt.subplots(figsize=(6, 8), facecolor="white")
|
| 294 |
valid = df[cancer_type].fillna(0).values.reshape(-1, 1)
|
|
|
|
| 295 |
cmap = plt.colormaps.get_cmap("YlOrRd")
|
| 296 |
cmap.set_bad("white")
|
| 297 |
masked = np.ma.masked_where(df[cancer_type].isna().values.reshape(-1, 1), valid)
|
|
|
|
| 324 |
)
|
| 325 |
|
| 326 |
gaps_md = "\n\n---\n\n".join(gap_cards) if gap_cards else "No data available."
|
| 327 |
+
journal_log("S2-A·R1a", f"cancer={cancer_type}", f"gaps={[p for p,_ in sorted_procs[:5]]}")
|
| 328 |
source_note = f"*Source: PubMed E-utilities | Date: {today}*"
|
| 329 |
return img, gaps_md + "\n\n" + source_note
|
| 330 |
|
|
|
|
| 458 |
f"and replace `_load_depmap_sample()` in `app.py`."
|
| 459 |
)
|
| 460 |
if not result_df.empty:
|
| 461 |
+
journal_log("S2-A·R1b", f"cancer={cancer_type}", f"top_gap={result_df.iloc[0]['Gene']}")
|
| 462 |
else:
|
| 463 |
+
journal_log("S2-A·R1b", f"cancer={cancer_type}", "no targets found")
|
| 464 |
return result_df, note
|
| 465 |
|
| 466 |
|
|
|
|
| 586 |
)
|
| 587 |
|
| 588 |
result_parts.append(f"\n*Source: ClinVar E-utilities + gnomAD GraphQL | Date: {today}*")
|
| 589 |
+
journal_log("S2-A·R1c", f"hgvs={hgvs}", result_parts[0][:100] if result_parts else "no results")
|
| 590 |
return "\n\n".join(result_parts)
|
| 591 |
|
| 592 |
|
|
|
|
| 652 |
|
| 653 |
summary = "\n\n".join(gap_text)
|
| 654 |
summary += f"\n\n*Source: PubMed E-utilities | Date: {today}*"
|
| 655 |
+
journal_log("S2-A·R1d", f"cancer={cancer_type}, kw={keyword}", summary[:100])
|
| 656 |
return img, summary
|
| 657 |
|
| 658 |
|
|
|
|
| 749 |
f"*Source: OpenTargets GraphQL + ClinicalTrials.gov v2 | Date: {today}*\n\n"
|
| 750 |
f"*Orphan = no approved drug (OpenTargets knownDrugs.count = 0)*"
|
| 751 |
)
|
| 752 |
+
journal_log("S2-A·R1e", f"cancer={cancer_type}", f"orphans={len(df)}")
|
| 753 |
return df, note
|
| 754 |
+
|
| 755 |
+
|
| 756 |
# ─────────────────────────────────────────────
|
| 757 |
# GROUP B — LEARNING SANDBOX
|
| 758 |
# ─────────────────────────────────────────────
|
|
|
|
| 832 |
"Expression log2FC": changes,
|
| 833 |
})
|
| 834 |
context = f"\n\n**Cancer Context:** {db['cancer_context']}"
|
| 835 |
+
journal_log("S2-B·R1a", f"gene={gene}", f"top_miRNA={mirnas[0]}")
|
| 836 |
return img, df.to_markdown(index=False) + context
|
| 837 |
|
| 838 |
|
|
|
|
| 894 |
"Off-target Risk": off_risk,
|
| 895 |
"Delivery Challenge": delivery,
|
| 896 |
})
|
| 897 |
+
journal_log("S2-B·R1b", f"cancer={cancer}", f"top={targets[0]}")
|
| 898 |
return img, df.to_markdown(index=False)
|
| 899 |
|
| 900 |
|
|
|
|
| 950 |
+ ("High ApoE → enhanced brain/liver targeting via LDLR pathway." if apoe_pct > 25
|
| 951 |
else "Low ApoE → reduced receptor-mediated uptake.")
|
| 952 |
)
|
| 953 |
+
journal_log("S2-B·R1c", f"PEG={peg_mol_pct}%,size={particle_size_nm}nm", f"ApoE={apoe_pct:.1f}%")
|
| 954 |
return img, interpretation
|
| 955 |
|
| 956 |
|
|
|
|
| 994 |
"The Vroman effect describes sequential protein displacement: "
|
| 995 |
"abundant proteins (albumin) adsorb first, then are displaced by higher-affinity proteins (ApoE, fibrinogen)."
|
| 996 |
)
|
| 997 |
+
journal_log("S2-B·R1d", f"kon_alb={kon_albumin},kon_apoe={kon_apoe}", note[:80])
|
| 998 |
return img, note
|
| 999 |
|
| 1000 |
|
|
|
|
| 1048 |
f"> ⚠️ SIMULATED — This is a rule-based educational model only. "
|
| 1049 |
f"Real variant classification requires expert review and full ACMG/AMP criteria evaluation."
|
| 1050 |
)
|
| 1051 |
+
journal_log("S2-B·R1e", f"class={classification}", output[:100])
|
| 1052 |
return output
|
|
|
|
|
|
|
| 1053 |
# ─────────────────────────────────────────────
|
| 1054 |
# GRADIO UI ASSEMBLY
|
| 1055 |
# ─────────────────────────────────────────────
|
|
|
|
| 1066 |
background: #f8f9fa; border-left: 4px solid #d73027;
|
| 1067 |
padding: 10px 14px; margin: 6px 0; border-radius: 4px;
|
| 1068 |
}
|
|
|
|
|
|
|
| 1069 |
/* Зміна курсора на pointer для всіх інтерактивних елементів */
|
| 1070 |
.gr-dropdown, .gr-button, .gr-slider, .gr-radio, .gr-checkbox,
|
| 1071 |
+
.tab-nav button, .gr-accordion, .gr-dataset {
|
| 1072 |
cursor: pointer !important;
|
| 1073 |
}
|
| 1074 |
/* Для текстових полів залишаємо звичайний текстовий курсор */
|
| 1075 |
.gr-textbox input, .gr-textarea textarea {
|
| 1076 |
cursor: text !important;
|
| 1077 |
}
|
| 1078 |
+
footer { display: none !important; }
|
| 1079 |
"""
|
| 1080 |
|
| 1081 |
|
|
|
|
| 1090 |
with gr.Row():
|
| 1091 |
# ── MAIN CONTENT ──
|
| 1092 |
with gr.Column(scale=4):
|
| 1093 |
+
with gr.Tabs() as main_tabs:
|
| 1094 |
|
| 1095 |
# ════════════════════════════════
|
| 1096 |
# GROUP A — REAL DATA TOOLS
|
|
|
|
| 1371 |
)
|
| 1372 |
b5_btn.click(b5_run, inputs=[b5_class], outputs=[b5_result])
|
| 1373 |
|
| 1374 |
+
# ════════════════════════════════
|
| 1375 |
+
# JOURNAL — окрема вкладка
|
| 1376 |
+
# ════════════════════════════════
|
| 1377 |
+
with gr.Tab("📓 Journal"):
|
| 1378 |
+
gr.Markdown("## Lab Journal — Full History")
|
| 1379 |
+
with gr.Row():
|
| 1380 |
+
journal_filter = gr.Dropdown(
|
| 1381 |
+
choices=["All"] + JOURNAL_CATEGORIES,
|
| 1382 |
+
value="All",
|
| 1383 |
+
label="Filter by category"
|
| 1384 |
+
)
|
| 1385 |
+
refresh_btn = gr.Button("🔄 Refresh", size="sm", variant="secondary")
|
| 1386 |
+
clear_btn = gr.Button("🗑️ Clear Journal", size="sm", variant="stop")
|
| 1387 |
+
journal_display = gr.Markdown(value=journal_read())
|
| 1388 |
+
|
| 1389 |
+
def refresh_journal(category):
|
| 1390 |
+
return journal_read(category)
|
| 1391 |
+
|
| 1392 |
+
refresh_btn.click(refresh_journal, inputs=[journal_filter], outputs=journal_display)
|
| 1393 |
+
clear_btn.click(clear_journal, [], journal_display).then(
|
| 1394 |
+
refresh_journal, inputs=[journal_filter], outputs=journal_display
|
| 1395 |
+
)
|
| 1396 |
+
journal_filter.change(refresh_journal, inputs=[journal_filter], outputs=journal_display)
|
| 1397 |
+
|
| 1398 |
+
# ── SIDEBAR (Quick Note) ──
|
| 1399 |
with gr.Column(scale=1, min_width=260):
|
| 1400 |
+
gr.Markdown("## 📝 Quick Note")
|
| 1401 |
note_category = gr.Dropdown(
|
| 1402 |
choices=JOURNAL_CATEGORIES,
|
| 1403 |
value="Manual",
|
|
|
|
| 1413 |
clear_note_btn = gr.Button("🗑️ Clear", size="sm")
|
| 1414 |
save_status = gr.Markdown("")
|
| 1415 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1416 |
def save_note(category, note):
|
| 1417 |
if note.strip():
|
| 1418 |
journal_log(category, "manual note", note, note)
|
| 1419 |
return "✅ Note saved.", ""
|
| 1420 |
return "⚠️ Note is empty.", ""
|
| 1421 |
|
|
|
|
|
|
|
|
|
|
| 1422 |
save_btn.click(
|
| 1423 |
save_note,
|
| 1424 |
inputs=[note_category, note_input],
|
| 1425 |
outputs=[save_status, note_input]
|
| 1426 |
)
|
| 1427 |
clear_note_btn.click(lambda: ("", ""), outputs=[note_input, save_status])
|
|
|
|
|
|
|
| 1428 |
|
| 1429 |
# === Інтеграція з Learning Playground ===
|
| 1430 |
with gr.Row():
|