Update app.py
Browse files
app.py
CHANGED
@@ -21,21 +21,20 @@ st.set_page_config(page_title="Pharma Research Expert Platform", layout="wide")
|
|
21 |
logging.basicConfig(level=logging.ERROR)
|
22 |
|
23 |
# -------------------------------
|
24 |
-
# API ENDPOINTS (Stable Sources
|
25 |
# -------------------------------
|
26 |
API_ENDPOINTS = {
|
27 |
-
"clinical_trials": "https://clinicaltrials.gov/api/v2/studies",
|
28 |
"pubchem": "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/{}/JSON",
|
29 |
"pubmed": "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
|
30 |
"fda_drug_approval": "https://api.fda.gov/drug/label.json",
|
31 |
"faers_adverse_events": "https://api.fda.gov/drug/event.json",
|
32 |
-
# PharmGKB
|
33 |
"pharmgkb_gene_variants": "https://api.pharmgkb.org/v1/data/gene/{}/variants",
|
34 |
-
#
|
35 |
-
"bioportal_search": "https://data.bioontology.org/search",
|
36 |
-
# RxNorm & RxClass endpoints
|
37 |
"rxnorm_rxcui": "https://rxnav.nlm.nih.gov/REST/rxcui.json",
|
38 |
"rxnorm_properties": "https://rxnav.nlm.nih.gov/REST/rxcui/{}/properties.json",
|
|
|
39 |
"rxclass_by_drug": "https://rxnav.nlm.nih.gov/REST/class/byDrugName.json"
|
40 |
}
|
41 |
|
@@ -44,22 +43,20 @@ API_ENDPOINTS = {
|
|
44 |
# -------------------------------
|
45 |
TRADE_TO_GENERIC = {
|
46 |
"tylenol": "acetaminophen",
|
|
|
47 |
"advil": "ibuprofen",
|
48 |
-
#
|
49 |
}
|
50 |
|
51 |
# -------------------------------
|
52 |
-
# SECRETS
|
53 |
# -------------------------------
|
54 |
OPENAI_API_KEY = st.secrets.get("OPENAI_API_KEY")
|
55 |
-
BIOPORTAL_API_KEY = st.secrets.get("BIOPORTAL_API_KEY")
|
56 |
-
PUB_EMAIL = st.secrets.get("PUB_EMAIL")
|
57 |
OPENFDA_KEY = st.secrets.get("OPENFDA_KEY")
|
|
|
58 |
|
59 |
if not PUB_EMAIL:
|
60 |
st.error("PUB_EMAIL is not configured in secrets.")
|
61 |
-
if not BIOPORTAL_API_KEY:
|
62 |
-
st.error("BIOPORTAL_API_KEY is not configured in secrets.")
|
63 |
if not OPENFDA_KEY:
|
64 |
st.error("OPENFDA_KEY is not configured in secrets.")
|
65 |
if not OPENAI_API_KEY:
|
@@ -72,7 +69,7 @@ from openai import OpenAI
|
|
72 |
openai_client = OpenAI(api_key=OPENAI_API_KEY)
|
73 |
|
74 |
def generate_ai_content(prompt: str) -> str:
|
75 |
-
"""
|
76 |
try:
|
77 |
response = openai_client.chat.completions.create(
|
78 |
model="gpt-4",
|
@@ -81,52 +78,28 @@ def generate_ai_content(prompt: str) -> str:
|
|
81 |
)
|
82 |
return response.choices[0].message.content.strip()
|
83 |
except Exception as e:
|
84 |
-
st.error(f"GPT
|
85 |
logging.error(e)
|
86 |
return "AI content generation failed."
|
87 |
|
88 |
# -------------------------------
|
89 |
-
# UTILITY FUNCTIONS
|
90 |
# -------------------------------
|
91 |
@st.cache_data(show_spinner=False)
|
92 |
def query_api(endpoint: str, params: Optional[Dict] = None, headers: Optional[Dict] = None) -> Optional[Dict]:
|
93 |
-
"""
|
94 |
try:
|
95 |
response = requests.get(endpoint, params=params, headers=headers, timeout=15)
|
96 |
response.raise_for_status()
|
97 |
return response.json()
|
98 |
except Exception as e:
|
99 |
st.error(f"API error for {endpoint}: {e}")
|
100 |
-
logging.error(f"API error for {endpoint}: {e}")
|
101 |
-
return None
|
102 |
-
|
103 |
-
@st.cache_data(show_spinner=False)
|
104 |
-
def get_pubchem_smiles(drug_name: str) -> Optional[str]:
|
105 |
-
"""Retrieve canonical SMILES using PubChem."""
|
106 |
-
url = API_ENDPOINTS["pubchem"].format(drug_name)
|
107 |
-
data = query_api(url)
|
108 |
-
if data and data.get("PC_Compounds"):
|
109 |
-
for prop in data["PC_Compounds"][0].get("props", []):
|
110 |
-
if prop.get("name") == "Canonical SMILES":
|
111 |
-
return prop["value"]["sval"]
|
112 |
-
return None
|
113 |
-
|
114 |
-
def draw_molecule(smiles: str) -> Optional[Any]:
|
115 |
-
"""Generate a 2D molecule image using RDKit."""
|
116 |
-
try:
|
117 |
-
mol = Chem.MolFromSmiles(smiles)
|
118 |
-
if mol:
|
119 |
-
return Draw.MolToImage(mol)
|
120 |
-
else:
|
121 |
-
st.error("Invalid SMILES string provided.")
|
122 |
-
except Exception as e:
|
123 |
-
st.error(f"Error drawing molecule: {e}")
|
124 |
logging.error(e)
|
125 |
return None
|
126 |
|
127 |
@st.cache_data(show_spinner=False)
|
128 |
def get_pubchem_drug_details(drug_name: str) -> Optional[Dict[str, str]]:
|
129 |
-
"""Retrieve drug details from PubChem."""
|
130 |
url = API_ENDPOINTS["pubchem"].format(drug_name)
|
131 |
data = query_api(url)
|
132 |
details = {}
|
@@ -143,9 +116,23 @@ def get_pubchem_drug_details(drug_name: str) -> Optional[Dict[str, str]]:
|
|
143 |
return details
|
144 |
return None
|
145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
146 |
@st.cache_data(show_spinner=False)
|
147 |
def get_clinical_trials(query: str) -> Optional[Dict]:
|
148 |
-
"""Query ClinicalTrials.gov."""
|
149 |
if query.upper().startswith("NCT") and query[3:].isdigit():
|
150 |
params = {"id": query, "fmt": "json"}
|
151 |
else:
|
@@ -154,16 +141,13 @@ def get_clinical_trials(query: str) -> Optional[Dict]:
|
|
154 |
|
155 |
@st.cache_data(show_spinner=False)
|
156 |
def get_pubmed(query: str) -> Optional[Dict]:
|
157 |
-
"""Query PubMed."""
|
158 |
params = {"db": "pubmed", "term": query, "retmax": 10, "retmode": "json", "email": PUB_EMAIL}
|
159 |
return query_api(API_ENDPOINTS["pubmed"], params)
|
160 |
|
161 |
@st.cache_data(show_spinner=False)
|
162 |
def get_fda_approval(drug_name: str) -> Optional[Dict]:
|
163 |
-
"""Retrieve FDA approval
|
164 |
-
if not OPENFDA_KEY:
|
165 |
-
st.error("OpenFDA key not configured.")
|
166 |
-
return None
|
167 |
query = f'openfda.brand_name:"{drug_name}"'
|
168 |
params = {"api_key": OPENFDA_KEY, "search": query, "limit": 1}
|
169 |
data = query_api(API_ENDPOINTS["fda_drug_approval"], params)
|
@@ -173,30 +157,14 @@ def get_fda_approval(drug_name: str) -> Optional[Dict]:
|
|
173 |
|
174 |
@st.cache_data(show_spinner=False)
|
175 |
def analyze_adverse_events(drug_name: str, limit: int = 5) -> Optional[Dict]:
|
176 |
-
"""Retrieve
|
177 |
-
if not OPENFDA_KEY:
|
178 |
-
st.error("OpenFDA key not configured.")
|
179 |
-
return None
|
180 |
query = f'patient.drug.medicinalproduct:"{drug_name}"'
|
181 |
params = {"api_key": OPENFDA_KEY, "search": query, "limit": limit}
|
182 |
return query_api(API_ENDPOINTS["faers_adverse_events"], params)
|
183 |
|
184 |
-
@st.cache_data(show_spinner=False)
|
185 |
-
def get_pharmgkb_variants_for_gene(pharmgkb_gene_id: str) -> Optional[List[str]]:
|
186 |
-
"""Return variant IDs for a PharmGKB gene accession."""
|
187 |
-
if not pharmgkb_gene_id.startswith("PA"):
|
188 |
-
st.warning("Enter a valid PharmGKB gene accession (e.g., PA1234).")
|
189 |
-
return None
|
190 |
-
endpoint = API_ENDPOINTS["pharmgkb_gene_variants"].format(pharmgkb_gene_id)
|
191 |
-
data = query_api(endpoint)
|
192 |
-
if data and data.get("data"):
|
193 |
-
return [variant["id"] for variant in data["data"]]
|
194 |
-
st.warning(f"No variants found for PharmGKB gene {pharmgkb_gene_id}.")
|
195 |
-
return None
|
196 |
-
|
197 |
@st.cache_data(show_spinner=False)
|
198 |
def get_rxnorm_rxcui(drug_name: str) -> Optional[str]:
|
199 |
-
"""
|
200 |
url = f"{API_ENDPOINTS['rxnorm_rxcui']}?name={drug_name}"
|
201 |
data = query_api(url)
|
202 |
if data and "idGroup" in data and data["idGroup"].get("rxnormId"):
|
@@ -206,36 +174,79 @@ def get_rxnorm_rxcui(drug_name: str) -> Optional[str]:
|
|
206 |
|
207 |
@st.cache_data(show_spinner=False)
|
208 |
def get_rxnorm_properties(rxcui: str) -> Optional[Dict]:
|
209 |
-
"""
|
210 |
url = API_ENDPOINTS["rxnorm_properties"].format(rxcui)
|
211 |
return query_api(url)
|
212 |
|
213 |
@st.cache_data(show_spinner=False)
|
214 |
def get_rxclass_by_drug_name(drug_name: str) -> Optional[Dict]:
|
215 |
-
"""
|
216 |
url = f"{API_ENDPOINTS['rxclass_by_drug']}?drugName={drug_name}"
|
217 |
data = query_api(url)
|
218 |
-
|
219 |
-
|
220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
221 |
|
222 |
# -------------------------------
|
223 |
# AI-DRIVEN DRUG INSIGHTS
|
224 |
# -------------------------------
|
225 |
def generate_drug_insights(drug_name: str) -> str:
|
226 |
"""
|
227 |
-
Gather FDA, PubChem, RxNorm, and RxClass
|
228 |
for an innovative, bullet‑point drug analysis.
|
229 |
"""
|
230 |
query_name = TRADE_TO_GENERIC.get(drug_name.lower(), drug_name)
|
231 |
|
232 |
-
#
|
233 |
fda_info = get_fda_approval(query_name)
|
234 |
fda_status = "Not Approved"
|
235 |
if fda_info and fda_info.get("openfda", {}).get("brand_name"):
|
236 |
fda_status = ", ".join(fda_info["openfda"]["brand_name"])
|
237 |
|
238 |
-
#
|
239 |
pubchem_details = get_pubchem_drug_details(query_name)
|
240 |
if pubchem_details:
|
241 |
formula = pubchem_details.get("Molecular Formula", "N/A")
|
@@ -244,7 +255,7 @@ def generate_drug_insights(drug_name: str) -> str:
|
|
244 |
else:
|
245 |
formula = iupac = canon_smiles = "Not Available"
|
246 |
|
247 |
-
# RxNorm
|
248 |
rxnorm_id = get_rxnorm_rxcui(query_name)
|
249 |
if rxnorm_id:
|
250 |
rx_props = get_rxnorm_properties(rxnorm_id)
|
@@ -252,22 +263,25 @@ def generate_drug_insights(drug_name: str) -> str:
|
|
252 |
else:
|
253 |
rxnorm_info = "No RxNorm data available."
|
254 |
|
|
|
255 |
rxclass_data = get_rxclass_by_drug_name(query_name)
|
256 |
rxclass_info = rxclass_data if rxclass_data else "No RxClass data available."
|
257 |
|
258 |
-
# Construct prompt for GPT
|
259 |
prompt = (
|
260 |
-
f"
|
261 |
-
f"
|
262 |
-
f"**
|
263 |
-
f"
|
264 |
-
f"
|
|
|
|
|
265 |
f"**RxClass Info:** {rxclass_info}\n\n"
|
266 |
-
f"Include
|
267 |
-
f"- Pharmacogenomic considerations (
|
268 |
-
f"- Potential repurposing opportunities
|
269 |
-
f"- Regulatory
|
270 |
-
f"-
|
271 |
)
|
272 |
return generate_ai_content(prompt)
|
273 |
|
@@ -287,7 +301,7 @@ tabs = st.tabs([
|
|
287 |
|
288 |
# ----- Tab 1: Drug Development -----
|
289 |
with tabs[0]:
|
290 |
-
st.header("AI
|
291 |
target = st.text_input("Target Disease/Pathway:", placeholder="Enter disease mechanism or target")
|
292 |
target_gene = st.text_input("Target Gene (PharmGKB Accession):", placeholder="e.g., PA1234")
|
293 |
strategy = st.selectbox("Development Strategy:", ["First-in-class", "Me-too", "Repurposing", "Biologic"])
|
@@ -297,7 +311,7 @@ with tabs[0]:
|
|
297 |
plan_prompt = (
|
298 |
f"Develop a detailed drug development plan for treating {target} using a {strategy} strategy. "
|
299 |
"Include sections on target validation, lead optimization, preclinical testing, clinical trial design, "
|
300 |
-
"regulatory strategy, market analysis, competitive landscape, and
|
301 |
)
|
302 |
plan = generate_ai_content(plan_prompt)
|
303 |
st.subheader("Comprehensive Development Plan")
|
@@ -320,10 +334,17 @@ with tabs[0]:
|
|
320 |
if variants:
|
321 |
st.write("PharmGKB Variants:")
|
322 |
st.write(variants)
|
|
|
|
|
323 |
for vid in variants[:3]:
|
324 |
-
|
325 |
-
|
326 |
-
|
|
|
|
|
|
|
|
|
|
|
327 |
else:
|
328 |
st.write("No variants found for the specified PharmGKB gene accession.")
|
329 |
else:
|
@@ -345,7 +366,8 @@ with tabs[1]:
|
|
345 |
"Phase": study.get("protocolSection", {}).get("designModule", {}).get("phases", ["Not Available"])[0],
|
346 |
"Enrollment": study.get("protocolSection", {}).get("designModule", {}).get("enrollmentInfo", {}).get("count", "N/A")
|
347 |
})
|
348 |
-
|
|
|
349 |
else:
|
350 |
st.warning("No clinical trials found for the query.")
|
351 |
|
@@ -370,35 +392,34 @@ with tabs[1]:
|
|
370 |
else:
|
371 |
st.write("No adverse event data available.")
|
372 |
|
373 |
-
# ----- Tab 3: Molecular Profiling -----
|
374 |
with tabs[2]:
|
375 |
st.header("Advanced Molecular Profiling")
|
376 |
compound_input = st.text_input("Compound Identifier:", placeholder="Enter drug name, SMILES, or INN")
|
377 |
if st.button("Analyze Compound"):
|
378 |
-
with st.spinner("Querying PubChem for molecular structure..."):
|
|
|
379 |
query_compound = TRADE_TO_GENERIC.get(compound_input.lower(), compound_input)
|
380 |
-
|
381 |
-
if
|
382 |
-
|
383 |
-
if
|
384 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
385 |
else:
|
386 |
-
st.error("
|
387 |
-
pubchem_data = query_api(API_ENDPOINTS["pubchem"].format(query_compound))
|
388 |
-
if pubchem_data and pubchem_data.get("PC_Compounds"):
|
389 |
-
st.subheader("Physicochemical Properties")
|
390 |
-
props = pubchem_data["PC_Compounds"][0].get("props", [])
|
391 |
-
mw = next((prop["value"]["sval"] for prop in props if prop.get("name") == "Molecular Weight"), "N/A")
|
392 |
-
logp = next((prop["value"]["sval"] for prop in props if prop.get("name") == "LogP"), "N/A")
|
393 |
-
st.write(f"**Molecular Weight:** {mw}")
|
394 |
-
st.write(f"**LogP:** {logp}")
|
395 |
-
else:
|
396 |
-
st.error("Physicochemical properties not available.")
|
397 |
|
398 |
-
# ----- Tab 4: Regulatory
|
399 |
with tabs[3]:
|
400 |
st.header("Global Regulatory Monitoring")
|
401 |
-
st.markdown("**Note:**
|
402 |
drug_prod = st.text_input("Drug Product:", placeholder="Enter generic or brand name")
|
403 |
if st.button("Generate Regulatory Report"):
|
404 |
with st.spinner("Compiling regulatory data..."):
|
@@ -430,7 +451,7 @@ with tabs[3]:
|
|
430 |
f"**Canonical SMILES:** {canon_smiles}\n"
|
431 |
)
|
432 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
|
433 |
-
pdf_file =
|
434 |
if pdf_file:
|
435 |
with open(pdf_file, "rb") as f:
|
436 |
st.download_button("Download Regulatory Report (PDF)", data=f, file_name=f"{drug_prod}_report.pdf", mime="application/pdf")
|
@@ -450,25 +471,12 @@ with tabs[4]:
|
|
450 |
st.markdown(f"- [PMID: {pmid}](https://pubmed.ncbi.nlm.nih.gov/{pmid}/)")
|
451 |
else:
|
452 |
st.write("No PubMed results found.")
|
453 |
-
|
454 |
-
ont_query = st.text_input("Enter search query for Ontology:", placeholder="e.g., Alzheimer's disease")
|
455 |
-
ont_select = st.selectbox("Select Ontology", ["MESH", "NCIT", "GO", "SNOMEDCT"])
|
456 |
-
if st.button("Search BioPortal"):
|
457 |
-
with st.spinner("Searching BioPortal..."):
|
458 |
-
bioportal_results = _get_bioportal_data(ont_select, ont_query)
|
459 |
-
if bioportal_results and bioportal_results.get("collection"):
|
460 |
-
st.subheader(f"BioPortal Results for {ont_select}")
|
461 |
-
for item in bioportal_results["collection"]:
|
462 |
-
label = item.get("prefLabel", "N/A")
|
463 |
-
ont_id = item.get("@id", "N/A")
|
464 |
-
st.markdown(f"- **{label}** ({ont_id})")
|
465 |
-
else:
|
466 |
-
st.write("No ontology results found.")
|
467 |
|
468 |
# ----- Tab 6: Comprehensive Dashboard -----
|
469 |
with tabs[5]:
|
470 |
st.header("Comprehensive Dashboard")
|
471 |
-
#
|
472 |
kpi_fda = 5000
|
473 |
kpi_trials = 12000
|
474 |
kpi_pubs = 250000
|
@@ -486,11 +494,12 @@ with tabs[5]:
|
|
486 |
ax_trend.set_ylabel("Number of Approvals")
|
487 |
st.pyplot(fig_trend)
|
488 |
st.subheader("Gene-Variant-Drug Network (Sample)")
|
|
|
489 |
sample_gene = "CYP2C19"
|
490 |
sample_variants = ["rs4244285", "rs12248560"]
|
491 |
sample_annots = {"rs4244285": ["Clopidogrel", "Omeprazole"], "rs12248560": ["Sertraline"]}
|
492 |
try:
|
493 |
-
net_fig =
|
494 |
st.plotly_chart(net_fig, use_container_width=True)
|
495 |
except Exception as e:
|
496 |
st.error(f"Network graph error: {e}")
|
@@ -533,6 +542,5 @@ with tabs[7]:
|
|
533 |
with st.spinner("Generating AI-driven insights..."):
|
534 |
query_ai_drug = TRADE_TO_GENERIC.get(ai_drug.lower(), ai_drug)
|
535 |
insights_text = generate_drug_insights(query_ai_drug)
|
536 |
-
st.subheader("AI
|
537 |
st.markdown(insights_text)
|
538 |
-
|
|
|
21 |
logging.basicConfig(level=logging.ERROR)
|
22 |
|
23 |
# -------------------------------
|
24 |
+
# API ENDPOINTS (Using only Stable Sources)
|
25 |
# -------------------------------
|
26 |
API_ENDPOINTS = {
|
27 |
+
"clinical_trials": "https://clinicaltrials.gov/api/v2/studies", # No email required now
|
28 |
"pubchem": "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/{}/JSON",
|
29 |
"pubmed": "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
|
30 |
"fda_drug_approval": "https://api.fda.gov/drug/label.json",
|
31 |
"faers_adverse_events": "https://api.fda.gov/drug/event.json",
|
32 |
+
# PharmGKB endpoint for gene variants (if available)
|
33 |
"pharmgkb_gene_variants": "https://api.pharmgkb.org/v1/data/gene/{}/variants",
|
34 |
+
# RxNorm endpoints
|
|
|
|
|
35 |
"rxnorm_rxcui": "https://rxnav.nlm.nih.gov/REST/rxcui.json",
|
36 |
"rxnorm_properties": "https://rxnav.nlm.nih.gov/REST/rxcui/{}/properties.json",
|
37 |
+
# RxClass endpoint (may return no data, so we provide a fallback message)
|
38 |
"rxclass_by_drug": "https://rxnav.nlm.nih.gov/REST/class/byDrugName.json"
|
39 |
}
|
40 |
|
|
|
43 |
# -------------------------------
|
44 |
TRADE_TO_GENERIC = {
|
45 |
"tylenol": "acetaminophen",
|
46 |
+
"panadol": "acetaminophen",
|
47 |
"advil": "ibuprofen",
|
48 |
+
# Add additional mappings as needed
|
49 |
}
|
50 |
|
51 |
# -------------------------------
|
52 |
+
# RETRIEVE SECRETS
|
53 |
# -------------------------------
|
54 |
OPENAI_API_KEY = st.secrets.get("OPENAI_API_KEY")
|
|
|
|
|
55 |
OPENFDA_KEY = st.secrets.get("OPENFDA_KEY")
|
56 |
+
PUB_EMAIL = st.secrets.get("PUB_EMAIL")
|
57 |
|
58 |
if not PUB_EMAIL:
|
59 |
st.error("PUB_EMAIL is not configured in secrets.")
|
|
|
|
|
60 |
if not OPENFDA_KEY:
|
61 |
st.error("OPENFDA_KEY is not configured in secrets.")
|
62 |
if not OPENAI_API_KEY:
|
|
|
69 |
openai_client = OpenAI(api_key=OPENAI_API_KEY)
|
70 |
|
71 |
def generate_ai_content(prompt: str) -> str:
|
72 |
+
"""Generate innovative insights using GPT‑4."""
|
73 |
try:
|
74 |
response = openai_client.chat.completions.create(
|
75 |
model="gpt-4",
|
|
|
78 |
)
|
79 |
return response.choices[0].message.content.strip()
|
80 |
except Exception as e:
|
81 |
+
st.error(f"GPT‑4 generation error: {e}")
|
82 |
logging.error(e)
|
83 |
return "AI content generation failed."
|
84 |
|
85 |
# -------------------------------
|
86 |
+
# UTILITY FUNCTIONS (with caching)
|
87 |
# -------------------------------
|
88 |
@st.cache_data(show_spinner=False)
|
89 |
def query_api(endpoint: str, params: Optional[Dict] = None, headers: Optional[Dict] = None) -> Optional[Dict]:
|
90 |
+
"""HTTP GET with error handling and caching."""
|
91 |
try:
|
92 |
response = requests.get(endpoint, params=params, headers=headers, timeout=15)
|
93 |
response.raise_for_status()
|
94 |
return response.json()
|
95 |
except Exception as e:
|
96 |
st.error(f"API error for {endpoint}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
97 |
logging.error(e)
|
98 |
return None
|
99 |
|
100 |
@st.cache_data(show_spinner=False)
|
101 |
def get_pubchem_drug_details(drug_name: str) -> Optional[Dict[str, str]]:
|
102 |
+
"""Retrieve drug details (including molecular formula, IUPAC name, and SMILES) from PubChem."""
|
103 |
url = API_ENDPOINTS["pubchem"].format(drug_name)
|
104 |
data = query_api(url)
|
105 |
details = {}
|
|
|
116 |
return details
|
117 |
return None
|
118 |
|
119 |
+
def save_pdf_report(report_content: str, filename: str) -> Optional[str]:
|
120 |
+
"""Save a text report as a PDF file using FPDF."""
|
121 |
+
try:
|
122 |
+
pdf = FPDF()
|
123 |
+
pdf.add_page()
|
124 |
+
pdf.set_font("Arial", size=12)
|
125 |
+
pdf.multi_cell(0, 10, report_content)
|
126 |
+
pdf.output(filename)
|
127 |
+
return filename
|
128 |
+
except Exception as e:
|
129 |
+
st.error(f"Error saving PDF: {e}")
|
130 |
+
logging.error(e)
|
131 |
+
return None
|
132 |
+
|
133 |
@st.cache_data(show_spinner=False)
|
134 |
def get_clinical_trials(query: str) -> Optional[Dict]:
|
135 |
+
"""Query ClinicalTrials.gov (NCT number or term search)."""
|
136 |
if query.upper().startswith("NCT") and query[3:].isdigit():
|
137 |
params = {"id": query, "fmt": "json"}
|
138 |
else:
|
|
|
141 |
|
142 |
@st.cache_data(show_spinner=False)
|
143 |
def get_pubmed(query: str) -> Optional[Dict]:
|
144 |
+
"""Query PubMed using the given search term."""
|
145 |
params = {"db": "pubmed", "term": query, "retmax": 10, "retmode": "json", "email": PUB_EMAIL}
|
146 |
return query_api(API_ENDPOINTS["pubmed"], params)
|
147 |
|
148 |
@st.cache_data(show_spinner=False)
|
149 |
def get_fda_approval(drug_name: str) -> Optional[Dict]:
|
150 |
+
"""Retrieve FDA drug approval data using openFDA."""
|
|
|
|
|
|
|
151 |
query = f'openfda.brand_name:"{drug_name}"'
|
152 |
params = {"api_key": OPENFDA_KEY, "search": query, "limit": 1}
|
153 |
data = query_api(API_ENDPOINTS["fda_drug_approval"], params)
|
|
|
157 |
|
158 |
@st.cache_data(show_spinner=False)
|
159 |
def analyze_adverse_events(drug_name: str, limit: int = 5) -> Optional[Dict]:
|
160 |
+
"""Retrieve adverse event data from FAERS."""
|
|
|
|
|
|
|
161 |
query = f'patient.drug.medicinalproduct:"{drug_name}"'
|
162 |
params = {"api_key": OPENFDA_KEY, "search": query, "limit": limit}
|
163 |
return query_api(API_ENDPOINTS["faers_adverse_events"], params)
|
164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
165 |
@st.cache_data(show_spinner=False)
|
166 |
def get_rxnorm_rxcui(drug_name: str) -> Optional[str]:
|
167 |
+
"""Retrieve the RxCUI for a drug from RxNorm."""
|
168 |
url = f"{API_ENDPOINTS['rxnorm_rxcui']}?name={drug_name}"
|
169 |
data = query_api(url)
|
170 |
if data and "idGroup" in data and data["idGroup"].get("rxnormId"):
|
|
|
174 |
|
175 |
@st.cache_data(show_spinner=False)
|
176 |
def get_rxnorm_properties(rxcui: str) -> Optional[Dict]:
|
177 |
+
"""Retrieve RxNorm properties for a given RxCUI."""
|
178 |
url = API_ENDPOINTS["rxnorm_properties"].format(rxcui)
|
179 |
return query_api(url)
|
180 |
|
181 |
@st.cache_data(show_spinner=False)
|
182 |
def get_rxclass_by_drug_name(drug_name: str) -> Optional[Dict]:
|
183 |
+
"""Query RxClass for drug classification info. (Fallback if no data is returned.)"""
|
184 |
url = f"{API_ENDPOINTS['rxclass_by_drug']}?drugName={drug_name}"
|
185 |
data = query_api(url)
|
186 |
+
return data # May return None if no data is found
|
187 |
+
|
188 |
+
def create_variant_network(gene: str, variants: List[str], annotations: Dict[str, List[str]]) -> go.Figure:
|
189 |
+
"""Generate a gene-variant-drug network graph using NetworkX and Plotly."""
|
190 |
+
G = nx.Graph()
|
191 |
+
G.add_node(gene, color="lightblue")
|
192 |
+
for variant in variants:
|
193 |
+
G.add_node(variant, color="lightgreen")
|
194 |
+
G.add_edge(gene, variant)
|
195 |
+
for drug in annotations.get(variant, []):
|
196 |
+
if drug and drug != "N/A":
|
197 |
+
G.add_node(drug, color="lightcoral")
|
198 |
+
G.add_edge(variant, drug)
|
199 |
+
pos = nx.spring_layout(G)
|
200 |
+
edge_x, edge_y = [], []
|
201 |
+
for edge in G.edges():
|
202 |
+
x0, y0 = pos[edge[0]]
|
203 |
+
x1, y1 = pos[edge[1]]
|
204 |
+
edge_x.extend([x0, x1, None])
|
205 |
+
edge_y.extend([y0, y1, None])
|
206 |
+
edge_trace = go.Scatter(
|
207 |
+
x=edge_x, y=edge_y, line=dict(width=1, color="#888"),
|
208 |
+
hoverinfo="none", mode="lines"
|
209 |
+
)
|
210 |
+
node_x, node_y, node_text, node_color = [], [], [], []
|
211 |
+
for node in G.nodes():
|
212 |
+
x, y = pos[node]
|
213 |
+
node_x.append(x)
|
214 |
+
node_y.append(y)
|
215 |
+
node_text.append(node)
|
216 |
+
node_color.append(G.nodes[node].get("color", "gray"))
|
217 |
+
node_trace = go.Scatter(
|
218 |
+
x=node_x, y=node_y, mode="markers+text", hoverinfo="text",
|
219 |
+
text=node_text, textposition="bottom center",
|
220 |
+
marker=dict(color=node_color, size=12, line_width=2)
|
221 |
+
)
|
222 |
+
fig = go.Figure(data=[edge_trace, node_trace],
|
223 |
+
layout=go.Layout(
|
224 |
+
title=dict(text="Gene-Variant-Drug Network", font=dict(size=16)),
|
225 |
+
showlegend=False,
|
226 |
+
hovermode="closest",
|
227 |
+
margin=dict(b=20, l=5, r=5, t=40),
|
228 |
+
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
|
229 |
+
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False)
|
230 |
+
))
|
231 |
+
return fig
|
232 |
|
233 |
# -------------------------------
|
234 |
# AI-DRIVEN DRUG INSIGHTS
|
235 |
# -------------------------------
|
236 |
def generate_drug_insights(drug_name: str) -> str:
|
237 |
"""
|
238 |
+
Gather data from FDA, PubChem, RxNorm, and RxClass (using generic fallback) and build a GPT‑4 prompt
|
239 |
for an innovative, bullet‑point drug analysis.
|
240 |
"""
|
241 |
query_name = TRADE_TO_GENERIC.get(drug_name.lower(), drug_name)
|
242 |
|
243 |
+
# FDA Data
|
244 |
fda_info = get_fda_approval(query_name)
|
245 |
fda_status = "Not Approved"
|
246 |
if fda_info and fda_info.get("openfda", {}).get("brand_name"):
|
247 |
fda_status = ", ".join(fda_info["openfda"]["brand_name"])
|
248 |
|
249 |
+
# PubChem Data
|
250 |
pubchem_details = get_pubchem_drug_details(query_name)
|
251 |
if pubchem_details:
|
252 |
formula = pubchem_details.get("Molecular Formula", "N/A")
|
|
|
255 |
else:
|
256 |
formula = iupac = canon_smiles = "Not Available"
|
257 |
|
258 |
+
# RxNorm Data
|
259 |
rxnorm_id = get_rxnorm_rxcui(query_name)
|
260 |
if rxnorm_id:
|
261 |
rx_props = get_rxnorm_properties(rxnorm_id)
|
|
|
263 |
else:
|
264 |
rxnorm_info = "No RxNorm data available."
|
265 |
|
266 |
+
# RxClass Data
|
267 |
rxclass_data = get_rxclass_by_drug_name(query_name)
|
268 |
rxclass_info = rxclass_data if rxclass_data else "No RxClass data available."
|
269 |
|
270 |
+
# Construct prompt for GPT‑4
|
271 |
prompt = (
|
272 |
+
f"Provide an innovative, advanced drug analysis for '{drug_name}' (generic: {query_name}).\n\n"
|
273 |
+
f"**FDA Approval Status:** {fda_status}\n\n"
|
274 |
+
f"**PubChem Details:**\n"
|
275 |
+
f"- Molecular Formula: {formula}\n"
|
276 |
+
f"- IUPAC Name: {iupac}\n"
|
277 |
+
f"- Canonical SMILES: {canon_smiles}\n\n"
|
278 |
+
f"**RxNorm Info:** {rxnorm_info}\n\n"
|
279 |
f"**RxClass Info:** {rxclass_info}\n\n"
|
280 |
+
f"Include in bullet points:\n"
|
281 |
+
f"- Pharmacogenomic considerations (e.g. genetic variants impacting metabolism or toxicity)\n"
|
282 |
+
f"- Potential repurposing opportunities and innovative therapeutic insights\n"
|
283 |
+
f"- Regulatory challenges and suggestions for personalized medicine approaches\n"
|
284 |
+
f"- Forward‑looking recommendations for future research and integration of diverse data sources\n"
|
285 |
)
|
286 |
return generate_ai_content(prompt)
|
287 |
|
|
|
301 |
|
302 |
# ----- Tab 1: Drug Development -----
|
303 |
with tabs[0]:
|
304 |
+
st.header("AI‑Driven Drug Development Strategy")
|
305 |
target = st.text_input("Target Disease/Pathway:", placeholder="Enter disease mechanism or target")
|
306 |
target_gene = st.text_input("Target Gene (PharmGKB Accession):", placeholder="e.g., PA1234")
|
307 |
strategy = st.selectbox("Development Strategy:", ["First-in-class", "Me-too", "Repurposing", "Biologic"])
|
|
|
311 |
plan_prompt = (
|
312 |
f"Develop a detailed drug development plan for treating {target} using a {strategy} strategy. "
|
313 |
"Include sections on target validation, lead optimization, preclinical testing, clinical trial design, "
|
314 |
+
"regulatory strategy, market analysis, competitive landscape, and pharmacogenomic considerations."
|
315 |
)
|
316 |
plan = generate_ai_content(plan_prompt)
|
317 |
st.subheader("Comprehensive Development Plan")
|
|
|
334 |
if variants:
|
335 |
st.write("PharmGKB Variants:")
|
336 |
st.write(variants)
|
337 |
+
# Optionally, display network graph if variant annotations are available.
|
338 |
+
sample_annots = {}
|
339 |
for vid in variants[:3]:
|
340 |
+
# Here you would normally fetch annotations.
|
341 |
+
# For demonstration, we set a dummy list:
|
342 |
+
sample_annots[vid] = ["DrugA", "DrugB"]
|
343 |
+
try:
|
344 |
+
net_fig = create_variant_network(target_gene, variants[:3], sample_annots)
|
345 |
+
st.plotly_chart(net_fig, use_container_width=True)
|
346 |
+
except Exception as e:
|
347 |
+
st.error(f"Network graph error: {e}")
|
348 |
else:
|
349 |
st.write("No variants found for the specified PharmGKB gene accession.")
|
350 |
else:
|
|
|
366 |
"Phase": study.get("protocolSection", {}).get("designModule", {}).get("phases", ["Not Available"])[0],
|
367 |
"Enrollment": study.get("protocolSection", {}).get("designModule", {}).get("enrollmentInfo", {}).get("count", "N/A")
|
368 |
})
|
369 |
+
df_trials = pd.DataFrame(trial_data)
|
370 |
+
st.dataframe(df_trials)
|
371 |
else:
|
372 |
st.warning("No clinical trials found for the query.")
|
373 |
|
|
|
392 |
else:
|
393 |
st.write("No adverse event data available.")
|
394 |
|
395 |
+
# ----- Tab 3: Advanced Molecular Profiling -----
|
396 |
with tabs[2]:
|
397 |
st.header("Advanced Molecular Profiling")
|
398 |
compound_input = st.text_input("Compound Identifier:", placeholder="Enter drug name, SMILES, or INN")
|
399 |
if st.button("Analyze Compound"):
|
400 |
+
with st.spinner("Querying PubChem for molecular structure and properties..."):
|
401 |
+
# Use trade-to-generic mapping
|
402 |
query_compound = TRADE_TO_GENERIC.get(compound_input.lower(), compound_input)
|
403 |
+
pubchem_info = get_pubchem_drug_details(query_compound)
|
404 |
+
if pubchem_info:
|
405 |
+
smiles = pubchem_info.get("Canonical SMILES")
|
406 |
+
if smiles and smiles != "N/A":
|
407 |
+
mol_image = draw_molecule(smiles)
|
408 |
+
if mol_image:
|
409 |
+
st.image(mol_image, caption="2D Molecular Structure")
|
410 |
+
else:
|
411 |
+
st.error("Canonical SMILES not found for this compound.")
|
412 |
+
st.subheader("Physicochemical Properties")
|
413 |
+
st.write(f"**Molecular Formula:** {pubchem_info.get('Molecular Formula', 'N/A')}")
|
414 |
+
st.write(f"**IUPAC Name:** {pubchem_info.get('IUPAC Name', 'N/A')}")
|
415 |
+
st.write(f"**Canonical SMILES:** {pubchem_info.get('Canonical SMILES', 'N/A')}")
|
416 |
else:
|
417 |
+
st.error("PubChem details not available for the given compound.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
418 |
|
419 |
+
# ----- Tab 4: Global Regulatory Monitoring -----
|
420 |
with tabs[3]:
|
421 |
st.header("Global Regulatory Monitoring")
|
422 |
+
st.markdown("**Note:** This section focuses on FDA data and PubChem drug details due to limitations with EMA/WHO/DailyMed APIs.")
|
423 |
drug_prod = st.text_input("Drug Product:", placeholder="Enter generic or brand name")
|
424 |
if st.button("Generate Regulatory Report"):
|
425 |
with st.spinner("Compiling regulatory data..."):
|
|
|
451 |
f"**Canonical SMILES:** {canon_smiles}\n"
|
452 |
)
|
453 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
|
454 |
+
pdf_file = save_pdf_report(report_text, tmp.name)
|
455 |
if pdf_file:
|
456 |
with open(pdf_file, "rb") as f:
|
457 |
st.download_button("Download Regulatory Report (PDF)", data=f, file_name=f"{drug_prod}_report.pdf", mime="application/pdf")
|
|
|
471 |
st.markdown(f"- [PMID: {pmid}](https://pubmed.ncbi.nlm.nih.gov/{pmid}/)")
|
472 |
else:
|
473 |
st.write("No PubMed results found.")
|
474 |
+
# (Ontology search removed due to unreliable endpoints)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
475 |
|
476 |
# ----- Tab 6: Comprehensive Dashboard -----
|
477 |
with tabs[5]:
|
478 |
st.header("Comprehensive Dashboard")
|
479 |
+
# Static sample KPIs – these can be replaced with dynamic aggregated data in the future
|
480 |
kpi_fda = 5000
|
481 |
kpi_trials = 12000
|
482 |
kpi_pubs = 250000
|
|
|
494 |
ax_trend.set_ylabel("Number of Approvals")
|
495 |
st.pyplot(fig_trend)
|
496 |
st.subheader("Gene-Variant-Drug Network (Sample)")
|
497 |
+
# Sample network using dummy data
|
498 |
sample_gene = "CYP2C19"
|
499 |
sample_variants = ["rs4244285", "rs12248560"]
|
500 |
sample_annots = {"rs4244285": ["Clopidogrel", "Omeprazole"], "rs12248560": ["Sertraline"]}
|
501 |
try:
|
502 |
+
net_fig = create_variant_network(sample_gene, sample_variants, sample_annots)
|
503 |
st.plotly_chart(net_fig, use_container_width=True)
|
504 |
except Exception as e:
|
505 |
st.error(f"Network graph error: {e}")
|
|
|
542 |
with st.spinner("Generating AI-driven insights..."):
|
543 |
query_ai_drug = TRADE_TO_GENERIC.get(ai_drug.lower(), ai_drug)
|
544 |
insights_text = generate_drug_insights(query_ai_drug)
|
545 |
+
st.subheader("AI‑Driven Drug Analysis")
|
546 |
st.markdown(insights_text)
|
|