ohboyFtw commited on
Commit
cbfd052
Β·
verified Β·
1 Parent(s): 671aab5

Upload 4 files

Browse files
Files changed (4) hide show
  1. .streamlit/config.toml +14 -0
  2. README.md +80 -0
  3. app.py +1716 -0
  4. requirements.txt +8 -0
.streamlit/config.toml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [theme]
2
+ primaryColor = "#06b6d4"
3
+ backgroundColor = "#0a0e17"
4
+ secondaryBackgroundColor = "#111827"
5
+ textColor = "#f1f5f9"
6
+ font = "sans serif"
7
+
8
+ [server]
9
+ headless = true
10
+ enableCORS = false
11
+ enableXsrfProtection = false
12
+
13
+ [browser]
14
+ gatherUsageStats = false
README.md ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: BioAgent - AI Bioinformatics Demo
3
+ emoji: 🧬
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_file: app.py
8
+ pinned: false
9
+ license: mit
10
+ ---
11
+
12
+ # 🧬 BioAgent: AI-Powered Bioinformatics
13
+
14
+ **BioAgent** demonstrates AI-augmented bioinformatics workflows combining Claude's reasoning capabilities with real scientific database APIs.
15
+
16
+ ## Features
17
+
18
+ ### πŸ’Š Drug Discovery Pipeline
19
+ - Search compounds via PubChem API (real data)
20
+ - Lipinski's Rule of Five assessment
21
+ - AI-powered drug-likeness analysis
22
+ - 2D structure visualization
23
+
24
+ ### 🧬 Sequence Analysis
25
+ - UniProt protein search (real data)
26
+ - Amino acid composition analysis
27
+ - AI-powered sequence interpretation
28
+ - Hydrophobicity and structure predictions
29
+
30
+ ### 🎯 Patient Stratification (NEW)
31
+ - Multi-omics integration (GEX, Mutations, CNV)
32
+ - Interactive UMAP patient clustering
33
+ - Modality attention visualization
34
+ - Kaplan-Meier survival analysis
35
+ - Biomarker discovery pipeline
36
+ - AI-generated clinical insights
37
+
38
+ ### πŸ“š Literature Intelligence
39
+ - PubMed search integration (real data)
40
+ - AI-synthesized research summaries
41
+ - Theme and gap identification
42
+ - Citation management
43
+
44
+ ### πŸ”¬ Single-Cell Analysis Demo
45
+ - Interactive UMAP visualization
46
+ - Cluster statistics
47
+ - AI-powered biological interpretation
48
+
49
+ ## Technology Stack
50
+
51
+ - **AI Backend**: Claude 3.5 Sonnet via Anthropic API
52
+ - **Scientific Skills**: [K-Dense Claude Scientific Skills](https://github.com/K-Dense-AI/claude-scientific-skills)
53
+ - **Data Sources**: PubChem, PubMed, UniProt (live APIs)
54
+ - **Frontend**: Streamlit with custom scientific theme
55
+
56
+ ## Usage
57
+
58
+ 1. Enter your Anthropic API key in the sidebar
59
+ 2. Select an analysis module
60
+ 3. Input your query (compound name, protein, sequence, etc.)
61
+ 4. Get AI-augmented insights combined with real data
62
+
63
+ ## Skills Integration
64
+
65
+ This demo implements Claude Scientific Skills as **context injection** - loading relevant SKILL.md documentation into the system prompt so Claude follows domain-specific best practices for:
66
+
67
+ - BioPython (sequence analysis)
68
+ - RDKit (molecular properties)
69
+ - Scanpy (single-cell analysis)
70
+ - PubMed/UniProt/ChEMBL APIs
71
+
72
+ ## Built By
73
+
74
+ **Excelra Data Sciences** - Powering drug discovery with AI-augmented bioinformatics.
75
+
76
+ Part of the **Risentia** platform for Bioinformaticians as a Service.
77
+
78
+ ## License
79
+
80
+ MIT License - Free for commercial and non-commercial use.
app.py ADDED
@@ -0,0 +1,1716 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BioAgent: AI-Powered Bioinformatics Demo
3
+ Deployable on HuggingFace Spaces with Claude Scientific Skills integration
4
+ """
5
+
6
+ import streamlit as st
7
+ import anthropic
8
+ import requests
9
+ import json
10
+ import time
11
+ import pandas as pd
12
+ import plotly.express as px
13
+ import plotly.graph_objects as go
14
+ from functools import lru_cache
15
+ from typing import Optional
16
+ import numpy as np
17
+ from io import StringIO
18
+ import re
19
+ from dataclasses import dataclass
20
+ from typing import Tuple
21
+
22
+ # ============================================================================
23
+ # PAGE CONFIG & STYLING
24
+ # ============================================================================
25
+
26
+ st.set_page_config(
27
+ page_title="BioAgent | AI Bioinformatics",
28
+ page_icon="🧬",
29
+ layout="wide",
30
+ initial_sidebar_state="expanded"
31
+ )
32
+
33
+ # Custom CSS for a distinctive scientific aesthetic
34
+ st.markdown("""
35
+ <style>
36
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Source+Sans+3:wght@300;400;600;700&display=swap');
37
+
38
+ :root {
39
+ --bg-primary: #0a0e17;
40
+ --bg-secondary: #111827;
41
+ --accent-cyan: #06b6d4;
42
+ --accent-emerald: #10b981;
43
+ --accent-violet: #8b5cf6;
44
+ --text-primary: #f1f5f9;
45
+ --text-muted: #94a3b8;
46
+ --border-color: #1e293b;
47
+ }
48
+
49
+ .stApp {
50
+ background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
51
+ }
52
+
53
+ .main-header {
54
+ font-family: 'Source Sans 3', sans-serif;
55
+ font-size: 2.5rem;
56
+ font-weight: 700;
57
+ background: linear-gradient(90deg, var(--accent-cyan), var(--accent-emerald));
58
+ -webkit-background-clip: text;
59
+ -webkit-text-fill-color: transparent;
60
+ background-clip: text;
61
+ margin-bottom: 0.5rem;
62
+ }
63
+
64
+ .sub-header {
65
+ font-family: 'Source Sans 3', sans-serif;
66
+ color: var(--text-muted);
67
+ font-size: 1.1rem;
68
+ font-weight: 300;
69
+ letter-spacing: 0.5px;
70
+ }
71
+
72
+ .module-card {
73
+ background: rgba(17, 24, 39, 0.8);
74
+ border: 1px solid var(--border-color);
75
+ border-radius: 12px;
76
+ padding: 1.5rem;
77
+ margin: 1rem 0;
78
+ backdrop-filter: blur(10px);
79
+ }
80
+
81
+ .metric-box {
82
+ background: linear-gradient(135deg, rgba(6, 182, 212, 0.1), rgba(16, 185, 129, 0.1));
83
+ border: 1px solid rgba(6, 182, 212, 0.3);
84
+ border-radius: 8px;
85
+ padding: 1rem;
86
+ text-align: center;
87
+ }
88
+
89
+ .code-output {
90
+ font-family: 'JetBrains Mono', monospace;
91
+ background: #0d1117;
92
+ border: 1px solid #30363d;
93
+ border-radius: 8px;
94
+ padding: 1rem;
95
+ font-size: 0.85rem;
96
+ overflow-x: auto;
97
+ }
98
+
99
+ .skill-badge {
100
+ display: inline-block;
101
+ background: rgba(139, 92, 246, 0.2);
102
+ border: 1px solid rgba(139, 92, 246, 0.5);
103
+ color: #a78bfa;
104
+ padding: 0.25rem 0.75rem;
105
+ border-radius: 9999px;
106
+ font-size: 0.75rem;
107
+ font-family: 'JetBrains Mono', monospace;
108
+ margin-right: 0.5rem;
109
+ margin-bottom: 0.5rem;
110
+ }
111
+
112
+ .status-running {
113
+ color: var(--accent-cyan);
114
+ animation: pulse 2s infinite;
115
+ }
116
+
117
+ @keyframes pulse {
118
+ 0%, 100% { opacity: 1; }
119
+ 50% { opacity: 0.5; }
120
+ }
121
+
122
+ /* Sidebar styling */
123
+ [data-testid="stSidebar"] {
124
+ background: rgba(10, 14, 23, 0.95);
125
+ border-right: 1px solid var(--border-color);
126
+ }
127
+
128
+ /* Input fields */
129
+ .stTextInput input, .stTextArea textarea, .stSelectbox select {
130
+ background: var(--bg-secondary) !important;
131
+ border: 1px solid var(--border-color) !important;
132
+ color: var(--text-primary) !important;
133
+ font-family: 'Source Sans 3', sans-serif !important;
134
+ }
135
+
136
+ /* Buttons */
137
+ .stButton > button {
138
+ background: linear-gradient(90deg, var(--accent-cyan), var(--accent-emerald));
139
+ color: var(--bg-primary);
140
+ font-weight: 600;
141
+ border: none;
142
+ border-radius: 8px;
143
+ padding: 0.5rem 2rem;
144
+ font-family: 'Source Sans 3', sans-serif;
145
+ transition: transform 0.2s, box-shadow 0.2s;
146
+ }
147
+
148
+ .stButton > button:hover {
149
+ transform: translateY(-2px);
150
+ box-shadow: 0 4px 20px rgba(6, 182, 212, 0.4);
151
+ }
152
+
153
+ /* Expander */
154
+ .streamlit-expanderHeader {
155
+ background: var(--bg-secondary);
156
+ border: 1px solid var(--border-color);
157
+ border-radius: 8px;
158
+ }
159
+
160
+ /* Tabs */
161
+ .stTabs [data-baseweb="tab-list"] {
162
+ gap: 8px;
163
+ background: transparent;
164
+ }
165
+
166
+ .stTabs [data-baseweb="tab"] {
167
+ background: var(--bg-secondary);
168
+ border: 1px solid var(--border-color);
169
+ border-radius: 8px 8px 0 0;
170
+ color: var(--text-muted);
171
+ font-family: 'Source Sans 3', sans-serif;
172
+ }
173
+
174
+ .stTabs [aria-selected="true"] {
175
+ background: linear-gradient(90deg, rgba(6, 182, 212, 0.2), rgba(16, 185, 129, 0.2));
176
+ border-color: var(--accent-cyan);
177
+ color: var(--text-primary);
178
+ }
179
+ </style>
180
+ """, unsafe_allow_html=True)
181
+
182
+ # ============================================================================
183
+ # SKILL MANAGEMENT
184
+ # ============================================================================
185
+
186
+ SKILL_BASE_URL = "https://raw.githubusercontent.com/K-Dense-AI/claude-scientific-skills/main"
187
+
188
+ SKILL_REGISTRY = {
189
+ "biopython": {
190
+ "path": "scientific-packages/biopython",
191
+ "description": "Sequence analysis, alignment, parsing biological data",
192
+ "icon": "🧬"
193
+ },
194
+ "rdkit": {
195
+ "path": "scientific-packages/rdkit",
196
+ "description": "Molecular properties, SMILES, fingerprints, drug discovery",
197
+ "icon": "πŸ’Š"
198
+ },
199
+ "scanpy": {
200
+ "path": "scientific-packages/scanpy",
201
+ "description": "Single-cell RNA-seq analysis, clustering, visualization",
202
+ "icon": "πŸ”¬"
203
+ },
204
+ "pubmed": {
205
+ "path": "scientific-databases/pubmed",
206
+ "description": "Literature search, citation retrieval",
207
+ "icon": "πŸ“š"
208
+ },
209
+ "uniprot": {
210
+ "path": "scientific-databases/uniprot",
211
+ "description": "Protein sequences, annotations, functional data",
212
+ "icon": "πŸ”·"
213
+ },
214
+ "chembl": {
215
+ "path": "scientific-databases/chembl",
216
+ "description": "Bioactive molecules, drug targets, bioassays",
217
+ "icon": "βš—οΈ"
218
+ },
219
+ "pubchem": {
220
+ "path": "scientific-databases/pubchem",
221
+ "description": "Chemical structures, properties, bioactivities",
222
+ "icon": "πŸ§ͺ"
223
+ }
224
+ }
225
+
226
+
227
+ @lru_cache(maxsize=20)
228
+ def fetch_skill(skill_key: str) -> str:
229
+ """Fetch a SKILL.md from K-Dense GitHub repo with caching"""
230
+ if skill_key not in SKILL_REGISTRY:
231
+ return ""
232
+
233
+ skill_path = SKILL_REGISTRY[skill_key]["path"]
234
+ url = f"{SKILL_BASE_URL}/{skill_path}/SKILL.md"
235
+
236
+ try:
237
+ response = requests.get(url, timeout=10)
238
+ if response.status_code == 200:
239
+ return response.text
240
+ except requests.RequestException:
241
+ pass
242
+
243
+ return ""
244
+
245
+
246
+ def get_combined_skills(skill_keys: list[str]) -> str:
247
+ """Combine multiple skills into a single context block"""
248
+ skills_content = []
249
+ for key in skill_keys:
250
+ content = fetch_skill(key)
251
+ if content:
252
+ skills_content.append(f"### {SKILL_REGISTRY[key]['icon']} {key.upper()} SKILL\n\n{content}")
253
+
254
+ return "\n\n---\n\n".join(skills_content)
255
+
256
+
257
+ # ============================================================================
258
+ # API INTEGRATIONS (Real Data)
259
+ # ============================================================================
260
+
261
+ class PubChemAPI:
262
+ """Real PubChem API integration"""
263
+ BASE_URL = "https://pubchem.ncbi.nlm.nih.gov/rest/pug"
264
+
265
+ @staticmethod
266
+ def search_compound(query: str, max_results: int = 5) -> list[dict]:
267
+ """Search for compounds by name"""
268
+ try:
269
+ # Get CIDs
270
+ url = f"{PubChemAPI.BASE_URL}/compound/name/{query}/cids/JSON"
271
+ response = requests.get(url, timeout=10)
272
+ if response.status_code != 200:
273
+ return []
274
+
275
+ cids = response.json().get("IdentifierList", {}).get("CID", [])[:max_results]
276
+
277
+ compounds = []
278
+ for cid in cids:
279
+ # Get properties
280
+ prop_url = f"{PubChemAPI.BASE_URL}/compound/cid/{cid}/property/MolecularFormula,MolecularWeight,XLogP,TPSA,HBondDonorCount,HBondAcceptorCount/JSON"
281
+ prop_response = requests.get(prop_url, timeout=10)
282
+
283
+ if prop_response.status_code == 200:
284
+ props = prop_response.json().get("PropertyTable", {}).get("Properties", [{}])[0]
285
+
286
+ def safe_float(val, default=0.0):
287
+ """Safely convert to float, handling None and strings"""
288
+ if val is None:
289
+ return default
290
+ try:
291
+ return float(val)
292
+ except (ValueError, TypeError):
293
+ return default
294
+
295
+ def safe_int(val, default=0):
296
+ """Safely convert to int, handling None and strings"""
297
+ if val is None:
298
+ return default
299
+ try:
300
+ return int(val)
301
+ except (ValueError, TypeError):
302
+ return default
303
+
304
+ compounds.append({
305
+ "cid": cid,
306
+ "formula": props.get("MolecularFormula") or "N/A",
307
+ "mw": safe_float(props.get("MolecularWeight")),
308
+ "logp": safe_float(props.get("XLogP")),
309
+ "tpsa": safe_float(props.get("TPSA")),
310
+ "hbd": safe_int(props.get("HBondDonorCount")),
311
+ "hba": safe_int(props.get("HBondAcceptorCount")),
312
+ "image_url": f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/{cid}/PNG"
313
+ })
314
+
315
+ return compounds
316
+ except Exception as e:
317
+ st.error(f"PubChem API error: {e}")
318
+ return []
319
+
320
+ @staticmethod
321
+ def get_structure_image(cid: int) -> str:
322
+ """Get 2D structure image URL"""
323
+ return f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/{cid}/PNG?image_size=300x300"
324
+
325
+
326
+ class PubMedAPI:
327
+ """Real PubMed/NCBI E-utilities integration"""
328
+ BASE_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
329
+
330
+ @staticmethod
331
+ def search_literature(query: str, max_results: int = 10) -> list[dict]:
332
+ """Search PubMed for articles"""
333
+ try:
334
+ # Search
335
+ search_url = f"{PubMedAPI.BASE_URL}/esearch.fcgi"
336
+ search_params = {
337
+ "db": "pubmed",
338
+ "term": query,
339
+ "retmax": max_results,
340
+ "retmode": "json",
341
+ "sort": "relevance"
342
+ }
343
+ search_response = requests.get(search_url, params=search_params, timeout=10)
344
+
345
+ if search_response.status_code != 200:
346
+ return []
347
+
348
+ pmids = search_response.json().get("esearchresult", {}).get("idlist", [])
349
+
350
+ if not pmids:
351
+ return []
352
+
353
+ # Fetch details
354
+ fetch_url = f"{PubMedAPI.BASE_URL}/esummary.fcgi"
355
+ fetch_params = {
356
+ "db": "pubmed",
357
+ "id": ",".join(pmids),
358
+ "retmode": "json"
359
+ }
360
+ fetch_response = requests.get(fetch_url, params=fetch_params, timeout=10)
361
+
362
+ if fetch_response.status_code != 200:
363
+ return []
364
+
365
+ results = fetch_response.json().get("result", {})
366
+ articles = []
367
+
368
+ for pmid in pmids:
369
+ if pmid in results:
370
+ article = results[pmid]
371
+ articles.append({
372
+ "pmid": pmid,
373
+ "title": article.get("title", "N/A"),
374
+ "authors": ", ".join([a.get("name", "") for a in article.get("authors", [])[:3]]),
375
+ "journal": article.get("source", "N/A"),
376
+ "pubdate": article.get("pubdate", "N/A"),
377
+ "url": f"https://pubmed.ncbi.nlm.nih.gov/{pmid}/"
378
+ })
379
+
380
+ return articles
381
+ except Exception as e:
382
+ st.error(f"PubMed API error: {e}")
383
+ return []
384
+
385
+
386
+ class UniProtAPI:
387
+ """Real UniProt API integration"""
388
+ BASE_URL = "https://rest.uniprot.org/uniprotkb"
389
+
390
+ @staticmethod
391
+ def search_protein(query: str, max_results: int = 5) -> list[dict]:
392
+ """Search UniProt for proteins"""
393
+ try:
394
+ url = f"{UniProtAPI.BASE_URL}/search"
395
+ params = {
396
+ "query": query,
397
+ "format": "json",
398
+ "size": max_results,
399
+ "fields": "accession,id,protein_name,organism_name,length,sequence"
400
+ }
401
+ response = requests.get(url, params=params, timeout=15)
402
+
403
+ if response.status_code != 200:
404
+ return []
405
+
406
+ results = response.json().get("results", [])
407
+ proteins = []
408
+
409
+ for r in results:
410
+ protein_name = r.get("proteinDescription", {}).get("recommendedName", {}).get("fullName", {}).get("value", "N/A")
411
+ proteins.append({
412
+ "accession": r.get("primaryAccession", "N/A"),
413
+ "entry_name": r.get("uniProtkbId", "N/A"),
414
+ "protein_name": protein_name,
415
+ "organism": r.get("organism", {}).get("scientificName", "N/A"),
416
+ "length": r.get("sequence", {}).get("length", 0),
417
+ "sequence": r.get("sequence", {}).get("value", "")[:100] + "..."
418
+ })
419
+
420
+ return proteins
421
+ except Exception as e:
422
+ st.error(f"UniProt API error: {e}")
423
+ return []
424
+
425
+
426
+ # ============================================================================
427
+ # CLAUDE INTEGRATION
428
+ # ============================================================================
429
+
430
+ def call_claude_with_skills(
431
+ client: anthropic.Anthropic,
432
+ user_prompt: str,
433
+ skill_keys: list[str],
434
+ task_context: str = "",
435
+ model: str = "claude-sonnet-4-20250514"
436
+ ) -> str:
437
+ """Call Claude API with scientific skills context injected"""
438
+
439
+ skills_content = get_combined_skills(skill_keys)
440
+
441
+ system_prompt = f"""You are BioAgent, an expert AI bioinformatics assistant created by Excelra.
442
+ You help researchers with drug discovery, multi-omics analysis, and precision medicine workflows.
443
+
444
+ {f"TASK CONTEXT: {task_context}" if task_context else ""}
445
+
446
+ SCIENTIFIC SKILLS REFERENCE:
447
+ Use the following skill documentation to inform your responses. Follow the patterns,
448
+ best practices, and code examples described in these skills.
449
+
450
+ <skills>
451
+ {skills_content}
452
+ </skills>
453
+
454
+ RESPONSE GUIDELINES:
455
+ 1. Be precise and scientifically accurate
456
+ 2. When providing code, use the libraries and patterns from the skills above
457
+ 3. Explain your reasoning step-by-step
458
+ 4. Cite relevant databases or literature when applicable
459
+ 5. Format output clearly with appropriate sections
460
+ 6. If generating analysis code, make it executable and well-commented
461
+ """
462
+
463
+ if client is None:
464
+ return "Error: No API client configured. Please enter your Anthropic API key."
465
+
466
+ try:
467
+ response = client.messages.create(
468
+ model=model,
469
+ max_tokens=4096,
470
+ system=system_prompt,
471
+ messages=[{"role": "user", "content": user_prompt}]
472
+ )
473
+ return response.content[0].text
474
+ except anthropic.APIError as e:
475
+ return f"API Error: {str(e)}"
476
+ except Exception as e:
477
+ return f"Error: {str(e)}"
478
+
479
+
480
+ # ============================================================================
481
+ # MODULE: DRUG DISCOVERY
482
+ # ============================================================================
483
+
484
+ def render_drug_discovery_module(client: anthropic.Anthropic):
485
+ """Drug Discovery Pipeline Module"""
486
+
487
+ st.markdown("### πŸ’Š Drug Discovery Pipeline")
488
+ st.markdown("Search compounds, analyze molecular properties, and get AI-powered insights.")
489
+
490
+ # Show active skills
491
+ st.markdown(
492
+ '<span class="skill-badge">rdkit</span>'
493
+ '<span class="skill-badge">pubchem</span>'
494
+ '<span class="skill-badge">chembl</span>',
495
+ unsafe_allow_html=True
496
+ )
497
+
498
+ col1, col2 = st.columns([2, 1])
499
+
500
+ with col1:
501
+ compound_query = st.text_input(
502
+ "Search Compound",
503
+ placeholder="e.g., aspirin, imatinib, remdesivir",
504
+ key="drug_search"
505
+ )
506
+
507
+ analysis_type = st.selectbox(
508
+ "Analysis Type",
509
+ ["Drug-likeness Assessment", "ADMET Prediction", "Target Identification", "Lead Optimization"]
510
+ )
511
+
512
+ with col2:
513
+ st.markdown("**Quick Examples:**")
514
+ st.caption("Click to copy, then paste:")
515
+ st.code("aspirin", language=None)
516
+ st.code("imatinib", language=None)
517
+ st.code("metformin", language=None)
518
+
519
+ if st.button("πŸ” Analyze Compound", key="analyze_drug") and compound_query:
520
+
521
+ with st.status("Running Drug Discovery Pipeline...", expanded=True) as status:
522
+
523
+ # Step 1: Fetch from PubChem
524
+ st.write("πŸ“‘ Querying PubChem database...")
525
+ compounds = PubChemAPI.search_compound(compound_query, max_results=3)
526
+ time.sleep(0.5)
527
+
528
+ if not compounds:
529
+ st.error(f"No compounds found for '{compound_query}'")
530
+ return
531
+
532
+ st.write(f"βœ“ Found {len(compounds)} compound(s)")
533
+
534
+ # Step 2: Display results
535
+ st.write("πŸ§ͺ Analyzing molecular properties...")
536
+ time.sleep(0.3)
537
+
538
+ status.update(label="Pipeline complete!", state="complete", expanded=True)
539
+
540
+ # Results display
541
+ tabs = st.tabs(["πŸ“Š Properties", "πŸ–ΌοΈ Structures", "πŸ€– AI Analysis"])
542
+
543
+ with tabs[0]:
544
+ # Properties table
545
+ df = pd.DataFrame(compounds)
546
+ df.columns = ["CID", "Formula", "MW (g/mol)", "LogP", "TPSA (Ε²)", "HBD", "HBA", "Image URL"]
547
+
548
+ # Lipinski's Rule of Five assessment
549
+ st.markdown("#### Lipinski's Rule of Five")
550
+
551
+ for comp in compounds:
552
+ violations = 0
553
+ checks = []
554
+
555
+ if comp["mw"] <= 500:
556
+ checks.append("βœ… MW ≀ 500")
557
+ else:
558
+ checks.append("❌ MW > 500")
559
+ violations += 1
560
+
561
+ if comp["logp"] <= 5:
562
+ checks.append("βœ… LogP ≀ 5")
563
+ else:
564
+ checks.append("❌ LogP > 5")
565
+ violations += 1
566
+
567
+ if comp["hbd"] <= 5:
568
+ checks.append("βœ… HBD ≀ 5")
569
+ else:
570
+ checks.append("❌ HBD > 5")
571
+ violations += 1
572
+
573
+ if comp["hba"] <= 10:
574
+ checks.append("βœ… HBA ≀ 10")
575
+ else:
576
+ checks.append("❌ HBA > 10")
577
+ violations += 1
578
+
579
+ with st.expander(f"CID {comp['cid']} - {comp['formula']} ({violations} violations)"):
580
+ col1, col2 = st.columns(2)
581
+ with col1:
582
+ for check in checks:
583
+ st.markdown(check)
584
+ with col2:
585
+ st.metric("Molecular Weight", f"{comp['mw']:.2f}")
586
+ st.metric("LogP", f"{comp['logp']:.2f}")
587
+
588
+ st.dataframe(df.drop(columns=["Image URL"]), width='stretch')
589
+
590
+ with tabs[1]:
591
+ # Structure images
592
+ cols = st.columns(min(len(compounds), 3))
593
+ for i, comp in enumerate(compounds):
594
+ with cols[i % 3]:
595
+ st.image(comp["image_url"], caption=f"CID: {comp['cid']}")
596
+
597
+ with tabs[2]:
598
+ # AI Analysis
599
+ with st.spinner("πŸ€– Generating AI analysis..."):
600
+ prompt = f"""Analyze the compound "{compound_query}" for {analysis_type.lower()}.
601
+
602
+ Available data from PubChem:
603
+ {json.dumps(compounds[0], indent=2)}
604
+
605
+ Provide:
606
+ 1. Brief compound overview
607
+ 2. Key molecular features relevant to {analysis_type.lower()}
608
+ 3. Potential concerns or advantages
609
+ 4. Recommended next steps for drug development
610
+
611
+ Be concise but thorough. Use scientific terminology appropriately."""
612
+
613
+ analysis = call_claude_with_skills(
614
+ client,
615
+ prompt,
616
+ ["rdkit", "pubchem", "chembl"],
617
+ task_context=f"Drug discovery analysis for {compound_query}"
618
+ )
619
+
620
+ st.markdown(analysis)
621
+
622
+
623
+ # ============================================================================
624
+ # MODULE: SEQUENCE ANALYSIS
625
+ # ============================================================================
626
+
627
+ def render_sequence_analysis_module(client: anthropic.Anthropic):
628
+ """Sequence Analysis Module"""
629
+
630
+ st.markdown("### 🧬 Sequence Analysis")
631
+ st.markdown("Analyze protein/DNA sequences with AI-powered insights.")
632
+
633
+ st.markdown(
634
+ '<span class="skill-badge">biopython</span>'
635
+ '<span class="skill-badge">uniprot</span>',
636
+ unsafe_allow_html=True
637
+ )
638
+
639
+ input_type = st.radio(
640
+ "Input Type",
641
+ ["Search UniProt", "Paste Sequence"],
642
+ horizontal=True
643
+ )
644
+
645
+ if input_type == "Search UniProt":
646
+ # Initialize session state for search results and analysis
647
+ if "uniprot_results" not in st.session_state:
648
+ st.session_state.uniprot_results = []
649
+ if "protein_analysis" not in st.session_state:
650
+ st.session_state.protein_analysis = {}
651
+
652
+ col1, col2 = st.columns([3, 1])
653
+ with col1:
654
+ protein_query = st.text_input(
655
+ "Search Protein",
656
+ placeholder="e.g., BRCA1, insulin, p53 human",
657
+ key="protein_search"
658
+ )
659
+ with col2:
660
+ st.markdown("**Examples:**")
661
+ st.caption("Click to copy, then paste:")
662
+ st.code("p53 human", language=None)
663
+ st.code("EGFR human", language=None)
664
+
665
+ if st.button("πŸ” Search UniProt") and protein_query:
666
+ with st.spinner("Searching UniProt..."):
667
+ results = UniProtAPI.search_protein(protein_query)
668
+ st.session_state.uniprot_results = results
669
+ st.session_state.protein_analysis = {} # Clear previous analyses
670
+ st.session_state.uniprot_searched = True # Track that search was performed
671
+
672
+ # Display results from session state (persists across reruns)
673
+ proteins = st.session_state.uniprot_results
674
+
675
+ if proteins:
676
+ st.markdown("#### Search Results")
677
+
678
+ # Let user select which protein to analyze
679
+ protein_options = {f"{p['entry_name']} - {p['protein_name'][:40]}": p for p in proteins}
680
+ selected_name = st.selectbox(
681
+ "Select protein to analyze:",
682
+ options=list(protein_options.keys()),
683
+ key="protein_selector"
684
+ )
685
+ selected_protein = protein_options[selected_name]
686
+ accession = selected_protein['accession']
687
+
688
+ # Show protein details
689
+ st.markdown(f"**Accession:** {accession} | **Organism:** {selected_protein['organism']} | **Length:** {selected_protein['length']} aa")
690
+
691
+ with st.expander("View sequence"):
692
+ st.code(selected_protein['sequence'], language=None)
693
+
694
+ # Analyze button - outside expander for reliability
695
+ if accession in st.session_state.protein_analysis:
696
+ st.markdown("---")
697
+ st.markdown("#### πŸ€– AI Analysis")
698
+ st.markdown(st.session_state.protein_analysis[accession])
699
+ else:
700
+ if st.button(f"πŸ”¬ Analyze {accession}", key="analyze_protein_btn"):
701
+ with st.spinner("Running sequence analysis with Claude..."):
702
+ prompt = f"""Analyze this protein:
703
+
704
+ Name: {selected_protein['protein_name']}
705
+ Organism: {selected_protein['organism']}
706
+ Length: {selected_protein['length']} amino acids
707
+ Sequence (truncated): {selected_protein['sequence']}
708
+
709
+ Provide:
710
+ 1. Functional overview of this protein
711
+ 2. Key domains/motifs likely present
712
+ 3. Disease associations (if known)
713
+ 4. Potential drug target considerations
714
+
715
+ Use your knowledge of protein biology and the BioPython skill for analysis approaches."""
716
+
717
+ analysis = call_claude_with_skills(
718
+ client,
719
+ prompt,
720
+ ["biopython", "uniprot"],
721
+ task_context="Protein sequence analysis"
722
+ )
723
+ if analysis:
724
+ st.session_state.protein_analysis[accession] = analysis
725
+ st.rerun()
726
+ else:
727
+ st.error("Failed to generate analysis. Check API key.")
728
+ elif st.session_state.get("uniprot_searched", False):
729
+ st.warning("No proteins found. Try a different search term.")
730
+
731
+ else: # Paste Sequence
732
+ sequence = st.text_area(
733
+ "Paste Sequence (FASTA or raw)",
734
+ height=150,
735
+ placeholder=">sequence_name\nMKTVRQERLKSIVRILERSKEPVSGAQLA..."
736
+ )
737
+
738
+ analysis_options = st.multiselect(
739
+ "Analysis Options",
740
+ ["Basic Statistics", "Composition Analysis", "Hydrophobicity Profile", "Secondary Structure Prediction"],
741
+ default=["Basic Statistics"]
742
+ )
743
+
744
+ if st.button("🧬 Analyze Sequence") and sequence:
745
+ # Parse sequence
746
+ clean_seq = "".join([line for line in sequence.split("\n") if not line.startswith(">")])
747
+ clean_seq = re.sub(r'[^A-Za-z]', '', clean_seq).upper()
748
+
749
+ if len(clean_seq) < 10:
750
+ st.error("Sequence too short. Please enter at least 10 residues.")
751
+ return
752
+
753
+ with st.spinner("Analyzing sequence..."):
754
+ # Basic calculations
755
+ seq_len = len(clean_seq)
756
+
757
+ # Amino acid composition
758
+ aa_counts = {aa: clean_seq.count(aa) for aa in "ACDEFGHIKLMNPQRSTVWY"}
759
+ aa_freq = {aa: count/seq_len*100 for aa, count in aa_counts.items()}
760
+
761
+ col1, col2, col3 = st.columns(3)
762
+ with col1:
763
+ st.metric("Length", f"{seq_len} aa")
764
+ with col2:
765
+ # Approximate MW
766
+ avg_aa_mw = 110 # Da
767
+ st.metric("Est. MW", f"{seq_len * avg_aa_mw / 1000:.1f} kDa")
768
+ with col3:
769
+ # Basic pI estimate (very rough)
770
+ acidic = clean_seq.count('D') + clean_seq.count('E')
771
+ basic = clean_seq.count('K') + clean_seq.count('R') + clean_seq.count('H')
772
+ if basic > acidic:
773
+ pi_est = "Basic (>7)"
774
+ elif acidic > basic:
775
+ pi_est = "Acidic (<7)"
776
+ else:
777
+ pi_est = "~Neutral"
778
+ st.metric("Est. pI", pi_est)
779
+
780
+ if "Composition Analysis" in analysis_options:
781
+ st.markdown("#### Amino Acid Composition")
782
+ fig = px.bar(
783
+ x=list(aa_freq.keys()),
784
+ y=list(aa_freq.values()),
785
+ labels={"x": "Amino Acid", "y": "Frequency (%)"},
786
+ color=list(aa_freq.values()),
787
+ color_continuous_scale="Viridis"
788
+ )
789
+ fig.update_layout(
790
+ template="plotly_dark",
791
+ paper_bgcolor="rgba(0,0,0,0)",
792
+ plot_bgcolor="rgba(0,0,0,0)"
793
+ )
794
+ st.plotly_chart(fig, width='stretch')
795
+
796
+ # AI Analysis
797
+ prompt = f"""Analyze this protein sequence:
798
+
799
+ Sequence length: {seq_len} amino acids
800
+ Sequence: {clean_seq[:200]}{'...' if len(clean_seq) > 200 else ''}
801
+
802
+ Amino acid composition highlights:
803
+ - Most frequent: {max(aa_freq, key=aa_freq.get)} ({aa_freq[max(aa_freq, key=aa_freq.get)]:.1f}%)
804
+ - Charged residues (D,E,K,R): {sum(aa_freq[aa] for aa in 'DEKR'):.1f}%
805
+ - Hydrophobic (A,V,I,L,M,F,W): {sum(aa_freq[aa] for aa in 'AVILMFW'):.1f}%
806
+
807
+ Requested analyses: {', '.join(analysis_options)}
808
+
809
+ Provide insights on:
810
+ 1. Likely protein type/family based on composition
811
+ 2. Notable sequence features
812
+ 3. Predicted structural characteristics
813
+ 4. Suggestions for experimental validation"""
814
+
815
+ analysis = call_claude_with_skills(
816
+ client,
817
+ prompt,
818
+ ["biopython", "uniprot"],
819
+ task_context="Sequence analysis"
820
+ )
821
+
822
+ st.markdown("#### AI Analysis")
823
+ st.markdown(analysis)
824
+
825
+
826
+ # ============================================================================
827
+ # MODULE: LITERATURE SEARCH
828
+ # ============================================================================
829
+
830
+ def render_literature_module(client: anthropic.Anthropic):
831
+ """Literature Search Module"""
832
+
833
+ st.markdown("### πŸ“š Literature Intelligence")
834
+ st.markdown("Search PubMed and get AI-synthesized insights from scientific literature.")
835
+
836
+ st.markdown(
837
+ '<span class="skill-badge">pubmed</span>',
838
+ unsafe_allow_html=True
839
+ )
840
+
841
+ col1, col2 = st.columns([3, 1])
842
+
843
+ with col1:
844
+ search_query = st.text_input(
845
+ "Search Query",
846
+ placeholder="e.g., CRISPR cancer therapy, single-cell RNA-seq tumor",
847
+ key="lit_search"
848
+ )
849
+
850
+ with col2:
851
+ max_results = st.slider("Max Results", 5, 20, 10)
852
+
853
+ search_type = st.radio(
854
+ "Search Mode",
855
+ ["Standard Search", "AI-Powered Synthesis"],
856
+ horizontal=True
857
+ )
858
+
859
+ if st.button("πŸ” Search Literature") and search_query:
860
+ with st.spinner("Searching PubMed..."):
861
+ articles = PubMedAPI.search_literature(search_query, max_results)
862
+
863
+ if not articles:
864
+ st.warning("No articles found. Try different search terms.")
865
+ return
866
+
867
+ st.success(f"Found {len(articles)} articles")
868
+
869
+ if search_type == "Standard Search":
870
+ for article in articles:
871
+ with st.expander(f"πŸ“„ {article['title'][:80]}..."):
872
+ st.markdown(f"**Authors:** {article['authors']}")
873
+ st.markdown(f"**Journal:** {article['journal']}")
874
+ st.markdown(f"**Date:** {article['pubdate']}")
875
+ st.markdown(f"**PMID:** [{article['pmid']}]({article['url']})")
876
+
877
+ else: # AI Synthesis
878
+ st.markdown("#### πŸ“Š Article Results")
879
+
880
+ # Show articles in compact format
881
+ df = pd.DataFrame(articles)
882
+ df = df[["pmid", "title", "journal", "pubdate"]]
883
+ st.dataframe(df, width='stretch', hide_index=True)
884
+
885
+ st.markdown("#### πŸ€– AI Synthesis")
886
+
887
+ with st.spinner("Synthesizing findings..."):
888
+ articles_text = "\n\n".join([
889
+ f"Title: {a['title']}\nAuthors: {a['authors']}\nJournal: {a['journal']} ({a['pubdate']})"
890
+ for a in articles
891
+ ])
892
+
893
+ prompt = f"""Based on these PubMed search results for "{search_query}":
894
+
895
+ {articles_text}
896
+
897
+ Provide a synthesis that includes:
898
+ 1. **Key Themes**: What are the main research directions evident from these papers?
899
+ 2. **Recent Advances**: What new findings or methods appear most significant?
900
+ 3. **Research Gaps**: Based on the titles, what areas might need more investigation?
901
+ 4. **Suggested Reading**: Which 2-3 papers seem most foundational for someone new to this topic?
902
+
903
+ Be concise and scientifically rigorous. Note that you only have titles/metadata, not full texts."""
904
+
905
+ synthesis = call_claude_with_skills(
906
+ client,
907
+ prompt,
908
+ ["pubmed"],
909
+ task_context=f"Literature synthesis for: {search_query}"
910
+ )
911
+
912
+ st.markdown(synthesis)
913
+
914
+
915
+ # ============================================================================
916
+ # MODULE: SINGLE-CELL ANALYSIS (Demo)
917
+ # ============================================================================
918
+
919
+ def render_single_cell_module(client: anthropic.Anthropic):
920
+ """Single-Cell Analysis Demo Module"""
921
+
922
+ st.markdown("### πŸ”¬ Single-Cell Analysis Demo")
923
+ st.markdown("Interactive demonstration of single-cell RNA-seq analysis concepts.")
924
+
925
+ st.markdown(
926
+ '<span class="skill-badge">scanpy</span>',
927
+ unsafe_allow_html=True
928
+ )
929
+
930
+ st.info("πŸ’‘ This module demonstrates single-cell analysis concepts using simulated data. "
931
+ "For real analysis, connect your own datasets.")
932
+
933
+ # Generate demo data
934
+ np.random.seed(42)
935
+ n_cells = 500
936
+ n_genes = 50
937
+
938
+ # Simulate 4 cell clusters
939
+ cluster_centers = [
940
+ np.array([2, 2]),
941
+ np.array([-2, 2]),
942
+ np.array([2, -2]),
943
+ np.array([-2, -2])
944
+ ]
945
+
946
+ cell_types = ["T cells", "B cells", "Monocytes", "NK cells"]
947
+
948
+ umap_coords = []
949
+ cell_labels = []
950
+
951
+ for i, center in enumerate(cluster_centers):
952
+ n_cluster = n_cells // 4
953
+ coords = np.random.randn(n_cluster, 2) * 0.5 + center
954
+ umap_coords.extend(coords)
955
+ cell_labels.extend([cell_types[i]] * n_cluster)
956
+
957
+ umap_coords = np.array(umap_coords)
958
+
959
+ # Create DataFrame
960
+ df = pd.DataFrame({
961
+ "UMAP1": umap_coords[:, 0],
962
+ "UMAP2": umap_coords[:, 1],
963
+ "Cell Type": cell_labels,
964
+ "n_genes": np.random.randint(500, 3000, len(cell_labels)),
965
+ "n_counts": np.random.randint(1000, 10000, len(cell_labels))
966
+ })
967
+
968
+ # Visualization options
969
+ color_by = st.selectbox(
970
+ "Color by",
971
+ ["Cell Type", "n_genes", "n_counts"]
972
+ )
973
+
974
+ # Create plot
975
+ if color_by == "Cell Type":
976
+ fig = px.scatter(
977
+ df,
978
+ x="UMAP1",
979
+ y="UMAP2",
980
+ color="Cell Type",
981
+ color_discrete_sequence=px.colors.qualitative.Set2,
982
+ hover_data=["n_genes", "n_counts"],
983
+ title="UMAP Projection - Cell Type Clusters"
984
+ )
985
+ else:
986
+ fig = px.scatter(
987
+ df,
988
+ x="UMAP1",
989
+ y="UMAP2",
990
+ color=color_by,
991
+ color_continuous_scale="Viridis",
992
+ hover_data=["Cell Type", "n_genes", "n_counts"],
993
+ title=f"UMAP Projection - {color_by}"
994
+ )
995
+
996
+ fig.update_layout(
997
+ template="plotly_dark",
998
+ paper_bgcolor="rgba(0,0,0,0)",
999
+ plot_bgcolor="rgba(0,0,0,0)",
1000
+ height=500
1001
+ )
1002
+ fig.update_traces(marker=dict(size=6, opacity=0.7))
1003
+
1004
+ st.plotly_chart(fig, width='stretch')
1005
+
1006
+ # Cluster statistics
1007
+ st.markdown("#### Cluster Statistics")
1008
+
1009
+ stats = df.groupby("Cell Type").agg({
1010
+ "n_genes": ["mean", "std"],
1011
+ "n_counts": ["mean", "std"],
1012
+ "UMAP1": "count"
1013
+ }).round(1)
1014
+ stats.columns = ["Avg Genes", "Std Genes", "Avg Counts", "Std Counts", "N Cells"]
1015
+
1016
+ st.dataframe(stats, width='stretch')
1017
+
1018
+ # AI interpretation
1019
+ if st.button("πŸ€– Get AI Interpretation"):
1020
+ with st.spinner("Generating interpretation..."):
1021
+ prompt = f"""Interpret this single-cell RNA-seq analysis result:
1022
+
1023
+ Dataset: {n_cells} cells, {n_genes} variable genes
1024
+ Clusters identified: {', '.join(cell_types)}
1025
+
1026
+ Cluster statistics:
1027
+ {stats.to_string()}
1028
+
1029
+ Provide:
1030
+ 1. Quality assessment of this dataset
1031
+ 2. Biological interpretation of the cell type distribution
1032
+ 3. Suggested downstream analyses
1033
+ 4. Potential validation experiments
1034
+
1035
+ This is simulated demonstration data, but analyze as if it were real."""
1036
+
1037
+ interpretation = call_claude_with_skills(
1038
+ client,
1039
+ prompt,
1040
+ ["scanpy"],
1041
+ task_context="Single-cell RNA-seq analysis interpretation"
1042
+ )
1043
+
1044
+ st.markdown(interpretation)
1045
+
1046
+
1047
+ # ============================================================================
1048
+ # MODULE: PATIENT STRATIFICATION (Multi-Omics VAE)
1049
+ # ============================================================================
1050
+
1051
+ class MultiOmicsStratificationEngine:
1052
+ """
1053
+ Multi-Omics Patient Stratification Engine
1054
+ Based on UCB Multi-Omics Stratification Agent architecture.
1055
+
1056
+ Features:
1057
+ - Simulated multi-omics data with causal relationships
1058
+ - Gated encoders with attention mechanism
1059
+ - UMAP visualization of patient clusters
1060
+ - Survival analysis simulation
1061
+ - Biomarker discovery
1062
+ """
1063
+
1064
+ def __init__(self, n_samples: int = 300, n_genes: int = 500, seed: int = 2025):
1065
+ self.n_samples = n_samples
1066
+ self.n_genes = n_genes
1067
+ self.seed = seed
1068
+ self.data = None
1069
+ self.results = None
1070
+
1071
+ def simulate_data(self) -> dict:
1072
+ """Simulate multi-omics data with biological relationships"""
1073
+ np.random.seed(self.seed)
1074
+
1075
+ # Patient subtypes: 0=Non-Responder, 1=Responder, 2=High-Risk
1076
+ subtypes = np.random.choice([0, 1, 2], self.n_samples, p=[0.4, 0.4, 0.2])
1077
+
1078
+ # CNV: mostly diploid (0), some gains (+1) and losses (-1)
1079
+ cnv = np.random.choice([-1, 0, 1], size=(self.n_samples, self.n_genes), p=[0.05, 0.9, 0.05])
1080
+
1081
+ # Mutations: sparse binary with subtype-specific patterns
1082
+ mutations = np.zeros((self.n_samples, self.n_genes))
1083
+ for i in range(self.n_samples):
1084
+ base_muts = np.random.random(self.n_genes) < 0.02
1085
+ if subtypes[i] == 0: # Non-responder: resistance mutations
1086
+ base_muts[0:50] = np.random.random(50) < 0.3
1087
+ elif subtypes[i] == 1: # Responder: sensitivity mutations
1088
+ base_muts[50:100] = np.random.random(50) < 0.3
1089
+ else: # High-risk: inflammatory signature
1090
+ base_muts[100:150] = np.random.random(50) < 0.25
1091
+ mutations[i] = base_muts.astype(int)
1092
+
1093
+ # Gene Expression: baseline + CNV effect + subtype signature + noise
1094
+ gex = np.random.randn(self.n_samples, self.n_genes) * 0.5
1095
+ gex += cnv * 0.8 # CNV affects expression
1096
+
1097
+ # Subtype-specific expression signatures
1098
+ for i in range(self.n_samples):
1099
+ if subtypes[i] == 0:
1100
+ gex[i, 0:50] += 1.5 # Resistance genes up
1101
+ gex[i, 200:250] -= 1.0 # Immune genes down
1102
+ elif subtypes[i] == 1:
1103
+ gex[i, 50:100] += 1.2 # Sensitivity pathway
1104
+ gex[i, 200:250] += 1.5 # Immune activation
1105
+ else:
1106
+ gex[i, 100:150] += 2.0 # Inflammatory genes
1107
+ gex[i, 150:200] += 1.0 # Cytokine storm signature
1108
+
1109
+ # Standardize GEX
1110
+ gex = (gex - gex.mean(axis=0)) / (gex.std(axis=0) + 1e-8)
1111
+
1112
+ # Patient metadata
1113
+ patient_ids = [f"PT{i:04d}" for i in range(self.n_samples)]
1114
+ ages = np.random.normal(55, 12, self.n_samples).astype(int)
1115
+ ages = np.clip(ages, 25, 85)
1116
+
1117
+ self.data = {
1118
+ 'gex': gex,
1119
+ 'mutations': mutations,
1120
+ 'cnv': cnv,
1121
+ 'subtypes': subtypes,
1122
+ 'patient_ids': patient_ids,
1123
+ 'ages': ages,
1124
+ 'n_samples': self.n_samples,
1125
+ 'n_genes': self.n_genes
1126
+ }
1127
+ return self.data
1128
+
1129
+ def run_stratification(self) -> dict:
1130
+ """
1131
+ Run patient stratification using PCA-based approach.
1132
+ (Simplified from full VAE for demo speed - real implementation uses PyTorch VAE)
1133
+ """
1134
+ if self.data is None:
1135
+ self.simulate_data()
1136
+
1137
+ from sklearn.decomposition import PCA
1138
+ from sklearn.preprocessing import StandardScaler
1139
+ from sklearn.ensemble import RandomForestClassifier
1140
+ from sklearn.model_selection import train_test_split
1141
+
1142
+ # Combine modalities with learned weights (simulated attention)
1143
+ # In full model, this comes from attention mechanism
1144
+ gex_weight = 0.55
1145
+ mut_weight = 0.30
1146
+ cnv_weight = 0.15
1147
+
1148
+ # Create combined feature matrix
1149
+ combined = np.hstack([
1150
+ self.data['gex'] * gex_weight,
1151
+ self.data['mutations'] * mut_weight,
1152
+ self.data['cnv'] * cnv_weight
1153
+ ])
1154
+
1155
+ # PCA for latent space (VAE substitute for demo)
1156
+ pca = PCA(n_components=32)
1157
+ latent = pca.fit_transform(combined)
1158
+
1159
+ # UMAP for visualization
1160
+ from sklearn.manifold import TSNE
1161
+ # Using t-SNE as fallback if UMAP not available
1162
+ try:
1163
+ import umap.umap_ as umap_lib
1164
+ reducer = umap_lib.UMAP(n_neighbors=15, min_dist=0.1, random_state=42)
1165
+ embedding = reducer.fit_transform(latent)
1166
+ except ImportError:
1167
+ tsne = TSNE(n_components=2, random_state=42, perplexity=30)
1168
+ embedding = tsne.fit_transform(latent)
1169
+
1170
+ # Train classifier
1171
+ X_train, X_test, y_train, y_test, idx_train, idx_test = train_test_split(
1172
+ latent, self.data['subtypes'], np.arange(self.n_samples),
1173
+ test_size=0.2, stratify=self.data['subtypes'], random_state=42
1174
+ )
1175
+
1176
+ clf = RandomForestClassifier(n_estimators=100, random_state=42)
1177
+ clf.fit(X_train, y_train)
1178
+
1179
+ # Predictions
1180
+ predictions = clf.predict(latent)
1181
+ probabilities = clf.predict_proba(latent)
1182
+ test_predictions = clf.predict(X_test)
1183
+ test_accuracy = (test_predictions == y_test).mean()
1184
+
1185
+ # Biomarker discovery: feature importance from GEX
1186
+ gex_importance = np.abs(pca.components_[:, :self.n_genes]).mean(axis=0)
1187
+ top_biomarkers = np.argsort(gex_importance)[-20:]
1188
+
1189
+ # Simulate survival times
1190
+ survival_times, events = self._simulate_survival(predictions)
1191
+
1192
+ self.results = {
1193
+ 'embedding': embedding,
1194
+ 'latent': latent,
1195
+ 'predictions': predictions,
1196
+ 'probabilities': probabilities,
1197
+ 'true_labels': self.data['subtypes'],
1198
+ 'test_accuracy': test_accuracy,
1199
+ 'attention_weights': np.array([gex_weight, mut_weight, cnv_weight]),
1200
+ 'top_biomarkers': top_biomarkers,
1201
+ 'biomarker_importance': gex_importance,
1202
+ 'survival_times': survival_times,
1203
+ 'survival_events': events,
1204
+ 'idx_test': idx_test
1205
+ }
1206
+ return self.results
1207
+
1208
+ def _simulate_survival(self, subtypes: np.ndarray) -> tuple:
1209
+ """Simulate survival times based on patient subtypes"""
1210
+ np.random.seed(self.seed + 1)
1211
+ times = []
1212
+ events = []
1213
+
1214
+ for s in subtypes:
1215
+ if s == 0: # Non-responder
1216
+ t = np.random.exponential(6)
1217
+ elif s == 1: # Responder
1218
+ t = np.random.exponential(24)
1219
+ else: # High-risk
1220
+ t = np.random.exponential(10)
1221
+
1222
+ # Censor at 24 months
1223
+ if t > 24:
1224
+ times.append(24)
1225
+ events.append(0)
1226
+ else:
1227
+ times.append(t)
1228
+ events.append(1)
1229
+
1230
+ return np.array(times), np.array(events)
1231
+
1232
+
1233
+ def render_patient_stratification_module(client: anthropic.Anthropic):
1234
+ """Patient Stratification Module - Multi-Omics Analysis"""
1235
+
1236
+ st.markdown("### 🎯 Patient Stratification")
1237
+ st.markdown("Multi-omics integration for clinical trial patient selection and risk stratification.")
1238
+
1239
+ st.markdown(
1240
+ '<span class="skill-badge">scanpy</span>'
1241
+ '<span class="skill-badge">biopython</span>'
1242
+ '<span class="skill-badge">pytorch</span>',
1243
+ unsafe_allow_html=True
1244
+ )
1245
+
1246
+ # Configuration
1247
+ st.markdown("#### βš™οΈ Configuration")
1248
+
1249
+ col1, col2, col3 = st.columns(3)
1250
+ with col1:
1251
+ n_patients = st.slider("Number of Patients", 100, 500, 300, step=50)
1252
+ with col2:
1253
+ n_genes = st.slider("Number of Genes", 200, 1000, 500, step=100)
1254
+ with col3:
1255
+ seed = st.number_input("Random Seed", value=2025, min_value=1)
1256
+
1257
+ data_source = st.radio(
1258
+ "Data Source",
1259
+ ["🎲 Simulated Demo Data", "πŸ“ Upload Real Data (Coming Soon)"],
1260
+ horizontal=True
1261
+ )
1262
+
1263
+ if "Upload" in data_source:
1264
+ st.info("πŸ“€ File upload for real multi-omics data will be available in production. "
1265
+ "Supported formats: CSV (GEX matrix), VCF (mutations), SEG (CNV).")
1266
+ return
1267
+
1268
+ if st.button("πŸš€ Run Stratification Pipeline", type="primary"):
1269
+
1270
+ # Initialize engine
1271
+ engine = MultiOmicsStratificationEngine(n_samples=n_patients, n_genes=n_genes, seed=seed)
1272
+
1273
+ with st.status("Running Multi-Omics Stratification Pipeline...", expanded=True) as status:
1274
+
1275
+ st.write("🧬 Simulating multi-omics data...")
1276
+ data = engine.simulate_data()
1277
+ time.sleep(0.3)
1278
+ st.write(f" βœ“ Generated {n_patients} patients Γ— {n_genes} genes")
1279
+ st.write(f" βœ“ Modalities: GEX, Mutations, CNV")
1280
+
1281
+ st.write("πŸ”§ Running stratification model...")
1282
+ results = engine.run_stratification()
1283
+ time.sleep(0.3)
1284
+ st.write(f" βœ“ Test accuracy: {results['test_accuracy']:.1%}")
1285
+
1286
+ st.write("πŸ“Š Generating visualizations...")
1287
+ time.sleep(0.2)
1288
+
1289
+ status.update(label="βœ… Pipeline Complete!", state="complete")
1290
+
1291
+ # Store in session state
1292
+ st.session_state['strat_engine'] = engine
1293
+ st.session_state['strat_results'] = results
1294
+ st.session_state['strat_data'] = data
1295
+
1296
+ # Display results if available
1297
+ if 'strat_results' in st.session_state:
1298
+ results = st.session_state['strat_results']
1299
+ data = st.session_state['strat_data']
1300
+ engine = st.session_state['strat_engine']
1301
+
1302
+ # Metrics row
1303
+ st.markdown("---")
1304
+ st.markdown("#### πŸ“ˆ Key Metrics")
1305
+
1306
+ col1, col2, col3, col4 = st.columns(4)
1307
+ with col1:
1308
+ st.metric("Patients", data['n_samples'])
1309
+ with col2:
1310
+ st.metric("Test Accuracy", f"{results['test_accuracy']:.1%}")
1311
+ with col3:
1312
+ n_responders = (results['predictions'] == 1).sum()
1313
+ st.metric("Predicted Responders", f"{n_responders} ({n_responders/data['n_samples']:.0%})")
1314
+ with col4:
1315
+ n_highrisk = (results['predictions'] == 2).sum()
1316
+ st.metric("High-Risk Flagged", n_highrisk)
1317
+
1318
+ # Tabs for visualizations
1319
+ tabs = st.tabs([
1320
+ "πŸ—ΊοΈ Patient Map",
1321
+ "βš–οΈ Modality Attention",
1322
+ "πŸ“‰ Survival Analysis",
1323
+ "🧬 Biomarkers",
1324
+ "πŸ€– AI Insights"
1325
+ ])
1326
+
1327
+ # Tab 1: Patient Stratification Map (UMAP)
1328
+ with tabs[0]:
1329
+ st.markdown("##### Patient Stratification Map")
1330
+ st.caption("UMAP projection of learned patient representations")
1331
+
1332
+ labels = ['Non-Responder', 'Responder', 'High-Risk']
1333
+ colors = ['#808080', '#2ecc71', '#e74c3c']
1334
+
1335
+ df_umap = pd.DataFrame({
1336
+ 'UMAP1': results['embedding'][:, 0],
1337
+ 'UMAP2': results['embedding'][:, 1],
1338
+ 'True Label': [labels[s] for s in data['subtypes']],
1339
+ 'Predicted': [labels[p] for p in results['predictions']],
1340
+ 'Patient ID': data['patient_ids'],
1341
+ 'Age': data['ages'],
1342
+ 'Responder Prob': results['probabilities'][:, 1]
1343
+ })
1344
+
1345
+ color_by = st.radio("Color by:", ["Predicted", "True Label", "Responder Prob"], horizontal=True)
1346
+
1347
+ if color_by == "Responder Prob":
1348
+ fig = px.scatter(
1349
+ df_umap, x='UMAP1', y='UMAP2',
1350
+ color='Responder Prob',
1351
+ color_continuous_scale='RdYlGn',
1352
+ hover_data=['Patient ID', 'Age', 'True Label', 'Predicted'],
1353
+ title='Patient Stratification Map'
1354
+ )
1355
+ else:
1356
+ fig = px.scatter(
1357
+ df_umap, x='UMAP1', y='UMAP2',
1358
+ color=color_by,
1359
+ color_discrete_map={
1360
+ 'Non-Responder': '#808080',
1361
+ 'Responder': '#2ecc71',
1362
+ 'High-Risk': '#e74c3c'
1363
+ },
1364
+ hover_data=['Patient ID', 'Age', 'Responder Prob'],
1365
+ title='Patient Stratification Map'
1366
+ )
1367
+
1368
+ fig.update_layout(
1369
+ template="plotly_dark",
1370
+ paper_bgcolor="rgba(0,0,0,0)",
1371
+ plot_bgcolor="rgba(0,0,0,0)",
1372
+ height=500
1373
+ )
1374
+ fig.update_traces(marker=dict(size=8, opacity=0.7, line=dict(width=0.5, color='white')))
1375
+ st.plotly_chart(fig, width='stretch')
1376
+
1377
+ # Confusion matrix
1378
+ st.markdown("##### Classification Performance")
1379
+ from sklearn.metrics import confusion_matrix, classification_report
1380
+
1381
+ cm = confusion_matrix(data['subtypes'], results['predictions'])
1382
+
1383
+ fig_cm = px.imshow(
1384
+ cm,
1385
+ labels=dict(x="Predicted", y="True", color="Count"),
1386
+ x=labels,
1387
+ y=labels,
1388
+ color_continuous_scale='Blues',
1389
+ text_auto=True
1390
+ )
1391
+ fig_cm.update_layout(
1392
+ template="plotly_dark",
1393
+ paper_bgcolor="rgba(0,0,0,0)",
1394
+ height=350
1395
+ )
1396
+ st.plotly_chart(fig_cm, width='stretch')
1397
+
1398
+ # Tab 2: Modality Attention
1399
+ with tabs[1]:
1400
+ st.markdown("##### Modality Importance")
1401
+ st.caption("Which omics data layer contributed most to stratification decisions?")
1402
+
1403
+ attn = results['attention_weights']
1404
+
1405
+ fig_attn = go.Figure(data=[
1406
+ go.Bar(
1407
+ x=['Gene Expression', 'Mutations', 'CNV'],
1408
+ y=attn,
1409
+ marker_color=['#4cc9f0', '#f72585', '#7209b7'],
1410
+ text=[f'{v:.0%}' for v in attn],
1411
+ textposition='outside'
1412
+ )
1413
+ ])
1414
+ fig_attn.update_layout(
1415
+ template="plotly_dark",
1416
+ paper_bgcolor="rgba(0,0,0,0)",
1417
+ plot_bgcolor="rgba(0,0,0,0)",
1418
+ yaxis_title="Attention Weight",
1419
+ yaxis_range=[0, 1],
1420
+ height=400,
1421
+ title="AI Attention: Which Data Drove the Decision?"
1422
+ )
1423
+ st.plotly_chart(fig_attn, width='stretch')
1424
+
1425
+ st.markdown(f"""
1426
+ **Interpretation:**
1427
+ - **Gene Expression** ({attn[0]:.0%}): Primary driver - captures pathway activity and treatment response signatures
1428
+ - **Mutations** ({attn[1]:.0%}): Key for identifying resistance/sensitivity markers
1429
+ - **CNV** ({attn[2]:.0%}): Supporting signal for dosage-sensitive genes
1430
+ """)
1431
+
1432
+ # Tab 3: Survival Analysis
1433
+ with tabs[2]:
1434
+ st.markdown("##### Simulated Clinical Trial: Survival Analysis")
1435
+ st.caption("Kaplan-Meier curves showing progression-free survival by predicted group")
1436
+
1437
+ fig_surv = go.Figure()
1438
+
1439
+ for group_idx, (group_name, color) in enumerate(zip(labels, colors)):
1440
+ mask = results['predictions'] == group_idx
1441
+ g_times = results['survival_times'][mask]
1442
+ g_events = results['survival_events'][mask]
1443
+
1444
+ # Sort by time
1445
+ sort_idx = np.argsort(g_times)
1446
+ g_times = g_times[sort_idx]
1447
+ g_events = g_events[sort_idx]
1448
+
1449
+ # Kaplan-Meier calculation
1450
+ survival_prob = 1.0
1451
+ probs_list = [1.0]
1452
+ ts = [0.0]
1453
+ n_at_risk = len(g_times)
1454
+
1455
+ for i, t in enumerate(g_times):
1456
+ if g_events[i] == 1:
1457
+ survival_prob *= (1 - 1/max(n_at_risk, 1))
1458
+ probs_list.append(survival_prob)
1459
+ ts.append(t)
1460
+ n_at_risk -= 1
1461
+
1462
+ fig_surv.add_trace(go.Scatter(
1463
+ x=ts, y=probs_list,
1464
+ mode='lines',
1465
+ name=f'{group_name} (n={mask.sum()})',
1466
+ line=dict(color=color, width=2, shape='hv')
1467
+ ))
1468
+
1469
+ fig_surv.update_layout(
1470
+ template="plotly_dark",
1471
+ paper_bgcolor="rgba(0,0,0,0)",
1472
+ plot_bgcolor="rgba(0,0,0,0)",
1473
+ xaxis_title="Time (months)",
1474
+ yaxis_title="Progression-Free Survival",
1475
+ yaxis_range=[0, 1.05],
1476
+ xaxis_range=[0, 25],
1477
+ height=450,
1478
+ title="Kaplan-Meier Survival Curves by Predicted Subtype",
1479
+ legend=dict(x=0.7, y=0.95)
1480
+ )
1481
+ st.plotly_chart(fig_surv, width='stretch')
1482
+
1483
+ # Median survival times
1484
+ st.markdown("**Median Progression-Free Survival:**")
1485
+ col1, col2, col3 = st.columns(3)
1486
+ for i, (col, label, color) in enumerate(zip([col1, col2, col3], labels, colors)):
1487
+ mask = results['predictions'] == i
1488
+ median_pfs = np.median(results['survival_times'][mask])
1489
+ with col:
1490
+ st.metric(label, f"{median_pfs:.1f} months")
1491
+
1492
+ # Tab 4: Biomarkers
1493
+ with tabs[3]:
1494
+ st.markdown("##### Biomarker Discovery")
1495
+ st.caption("Genes most predictive of treatment response")
1496
+
1497
+ top_genes = results['top_biomarkers'][-15:]
1498
+ importance = results['biomarker_importance'][top_genes]
1499
+
1500
+ fig_bio = go.Figure(data=[
1501
+ go.Bar(
1502
+ y=[f"Gene_{g:04d}" for g in top_genes],
1503
+ x=importance,
1504
+ orientation='h',
1505
+ marker_color='#2ecc71'
1506
+ )
1507
+ ])
1508
+ fig_bio.update_layout(
1509
+ template="plotly_dark",
1510
+ paper_bgcolor="rgba(0,0,0,0)",
1511
+ plot_bgcolor="rgba(0,0,0,0)",
1512
+ xaxis_title="Importance Score",
1513
+ height=450,
1514
+ title="Top 15 Predictive Biomarkers"
1515
+ )
1516
+ st.plotly_chart(fig_bio, width='stretch')
1517
+
1518
+ # Gene distribution by subtype
1519
+ st.markdown("##### Top Biomarker Expression by Subtype")
1520
+ top_gene = top_genes[-1]
1521
+
1522
+ df_gene = pd.DataFrame({
1523
+ 'Expression': data['gex'][:, top_gene],
1524
+ 'Subtype': [labels[s] for s in data['subtypes']]
1525
+ })
1526
+
1527
+ fig_box = px.box(
1528
+ df_gene, x='Subtype', y='Expression',
1529
+ color='Subtype',
1530
+ color_discrete_map={
1531
+ 'Non-Responder': '#808080',
1532
+ 'Responder': '#2ecc71',
1533
+ 'High-Risk': '#e74c3c'
1534
+ },
1535
+ title=f"Gene_{top_gene:04d} Expression by Subtype"
1536
+ )
1537
+ fig_box.update_layout(
1538
+ template="plotly_dark",
1539
+ paper_bgcolor="rgba(0,0,0,0)",
1540
+ plot_bgcolor="rgba(0,0,0,0)",
1541
+ showlegend=False,
1542
+ height=350
1543
+ )
1544
+ st.plotly_chart(fig_box, width='stretch')
1545
+
1546
+ # Tab 5: AI Insights
1547
+ with tabs[4]:
1548
+ st.markdown("##### πŸ€– Clinical Insights")
1549
+
1550
+ if st.button("Generate Analysis", key="ai_strat"):
1551
+ with st.spinner("Analyzing the stratification results..."):
1552
+
1553
+ # Build context
1554
+ subtype_counts = pd.Series(results['predictions']).value_counts().sort_index()
1555
+ top_genes = results['top_biomarkers']
1556
+
1557
+ prompt = f"""Analyze these patient stratification results from a multi-omics clinical trial analysis:
1558
+
1559
+ **Dataset Summary:**
1560
+ - Total patients: {data['n_samples']}
1561
+ - Features: {data['n_genes']} genes across 3 modalities (GEX, Mutations, CNV)
1562
+ - Test accuracy: {results['test_accuracy']:.1%}
1563
+
1564
+ **Stratification Results:**
1565
+ - Non-Responders: {subtype_counts.get(0, 0)} patients ({subtype_counts.get(0, 0)/data['n_samples']:.0%})
1566
+ - Responders: {subtype_counts.get(1, 0)} patients ({subtype_counts.get(1, 0)/data['n_samples']:.0%})
1567
+ - High-Risk: {subtype_counts.get(2, 0)} patients ({subtype_counts.get(2, 0)/data['n_samples']:.0%})
1568
+
1569
+ **Modality Importance:**
1570
+ - Gene Expression: {results['attention_weights'][0]:.0%}
1571
+ - Mutations: {results['attention_weights'][1]:.0%}
1572
+ - CNV: {results['attention_weights'][2]:.0%}
1573
+
1574
+ **Survival Analysis:**
1575
+ - Responders show significantly longer progression-free survival
1576
+ - High-risk patients flagged for potential adverse events
1577
+
1578
+ **Top Biomarker Genes:** Gene_{top_genes[-1]:04d}, Gene_{top_genes[-2]:04d}, Gene_{top_genes[-3]:04d}
1579
+
1580
+ Provide clinical insights covering:
1581
+ 1. **Patient Selection**: Recommendations for trial enrollment criteria
1582
+ 2. **Risk Stratification**: How to use these results for patient monitoring
1583
+ 3. **Biomarker Validation**: Next steps for validating discovered markers
1584
+ 4. **Trial Design**: How this stratification could improve trial efficiency
1585
+ 5. **Regulatory Considerations**: Key points for FDA submission
1586
+
1587
+ Be specific and actionable. This is for a precision medicine clinical trial context."""
1588
+
1589
+ analysis = call_claude_with_skills(
1590
+ client,
1591
+ prompt,
1592
+ ["scanpy", "biopython"],
1593
+ task_context="Multi-omics patient stratification for clinical trials"
1594
+ )
1595
+
1596
+ st.markdown(analysis)
1597
+ else:
1598
+ st.info("Click 'Generate AI Analysis' to get Claude's interpretation of the stratification results.")
1599
+
1600
+
1601
+ # ============================================================================
1602
+ # MAIN APPLICATION
1603
+ # ============================================================================
1604
+
1605
+ def main():
1606
+ # Header
1607
+ st.markdown('<h1 class="main-header">🧬 BioAgent</h1>', unsafe_allow_html=True)
1608
+ st.markdown('<p class="sub-header">AI-Powered Bioinformatics β€’ Drug Discovery β€’ Multi-Omics Analysis</p>', unsafe_allow_html=True)
1609
+
1610
+ # Sidebar
1611
+ with st.sidebar:
1612
+ st.markdown("## βš™οΈ Configuration")
1613
+
1614
+ # Try to get API key from secrets first (for HuggingFace Spaces)
1615
+ api_key_from_secrets = st.secrets.get("ANTHROPIC_API_KEY", None) if hasattr(st, 'secrets') else None
1616
+
1617
+ if api_key_from_secrets:
1618
+ api_key = api_key_from_secrets
1619
+ st.success("βœ“ API key loaded from secrets")
1620
+ else:
1621
+ api_key = st.text_input(
1622
+ "Anthropic API Key",
1623
+ type="password",
1624
+ help="Enter your Anthropic API key to enable AI features"
1625
+ )
1626
+ if api_key:
1627
+ st.success("βœ“ API key configured")
1628
+
1629
+ if api_key:
1630
+ client = anthropic.Anthropic(api_key=api_key)
1631
+ else:
1632
+ st.warning("⚠️ Enter API key for AI features")
1633
+ client = None
1634
+
1635
+ st.markdown("---")
1636
+
1637
+ st.markdown("## πŸ§ͺ Analysis Modules")
1638
+
1639
+ module = st.radio(
1640
+ "Select Module",
1641
+ [
1642
+ "πŸ’Š Drug Discovery",
1643
+ "🧬 Sequence Analysis",
1644
+ "🎯 Patient Stratification",
1645
+ "πŸ“š Literature Search",
1646
+ "πŸ”¬ Single-Cell Demo"
1647
+ ],
1648
+ label_visibility="collapsed"
1649
+ )
1650
+
1651
+ st.markdown("---")
1652
+
1653
+ st.markdown("## πŸ“– About")
1654
+ st.markdown("""
1655
+ **BioAgent** demonstrates Agentic
1656
+ bioinformatics workflows using:
1657
+
1658
+ - Scientific Skills
1659
+ - Multi-Omics Data Integration
1660
+ - Real-time database APIs
1661
+ - Interactive visualizations
1662
+ - Anthropic
1663
+
1664
+ Developed by Aravind [Risentia].""")
1665
+
1666
+ st.markdown("---")
1667
+
1668
+ # Skills status
1669
+ with st.expander("πŸ”§ Loaded Skills"):
1670
+ for key, info in SKILL_REGISTRY.items():
1671
+ st.markdown(f"{info['icon']} **{key}**")
1672
+ st.caption(info['description'])
1673
+
1674
+ # Main content
1675
+ if not client:
1676
+ st.markdown("""
1677
+ <div class="module-card">
1678
+ <h3>πŸ‘‹ Welcome to BioAgent</h3>
1679
+ <p>Enter your Anthropic API key in the sidebar to enable AI-powered analysis features.</p>
1680
+ <p>You can still explore the data integrations (PubChem, PubMed, UniProt) without an API key.</p>
1681
+ </div>
1682
+ """, unsafe_allow_html=True)
1683
+
1684
+ # Show demo even without API key
1685
+ st.markdown("### 🎯 Quick Demo: PubChem Search")
1686
+ demo_compound = st.text_input("Try searching a compound:", value="caffeine")
1687
+ if st.button("Search PubChem"):
1688
+ with st.spinner("Fetching from PubChem..."):
1689
+ compounds = PubChemAPI.search_compound(demo_compound)
1690
+ if compounds:
1691
+ for c in compounds:
1692
+ col1, col2 = st.columns([1, 2])
1693
+ with col1:
1694
+ st.image(c["image_url"])
1695
+ with col2:
1696
+ st.markdown(f"**CID:** {c['cid']}")
1697
+ st.markdown(f"**Formula:** {c['formula']}")
1698
+ st.markdown(f"**MW:** {c['mw']:.2f} g/mol")
1699
+ st.markdown(f"**LogP:** {c['logp']:.2f}")
1700
+ return
1701
+
1702
+ # Render selected module
1703
+ if "Drug Discovery" in module:
1704
+ render_drug_discovery_module(client)
1705
+ elif "Sequence Analysis" in module:
1706
+ render_sequence_analysis_module(client)
1707
+ elif "Patient Stratification" in module:
1708
+ render_patient_stratification_module(client)
1709
+ elif "Literature" in module:
1710
+ render_literature_module(client)
1711
+ elif "Single-Cell" in module:
1712
+ render_single_cell_module(client)
1713
+
1714
+
1715
+ if __name__ == "__main__":
1716
+ main()
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ streamlit>=1.28.0
2
+ anthropic>=0.40.0
3
+ pandas>=2.0.0
4
+ plotly>=5.18.0
5
+ requests>=2.31.0
6
+ numpy>=1.24.0
7
+ scikit-learn>=1.3.0
8
+ umap-learn>=0.5.4