From a91b9bb855e02b6b4fd662ef4bce1f87dee92e18 Mon Sep 17 00:00:00 2001 From: oluwadareab12 Date: Mon, 13 Apr 2026 22:58:51 -0700 Subject: [PATCH] =?UTF-8?q?feat(skills):=20add=20drug-discovery=20optional?= =?UTF-8?q?=20skill=20=E2=80=94=20ChEMBL,=20PubChem,=20OpenFDA,=20ADMET=20?= =?UTF-8?q?analysis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pharmaceutical research skill covering bioactive compound search (ChEMBL), drug-likeness screening (Lipinski Ro5 + Veber via PubChem), drug-drug interaction lookups (OpenFDA), gene-disease associations (OpenTargets GraphQL), and ADMET reasoning guidance. All free public APIs, zero auth, stdlib-only Python. Includes helper scripts for batch Ro5 screening and target-to-compound pipelines. Moved to optional-skills/research/ (niche domain skill, not built-in). Fixed: authors→author frontmatter, removed unused jq prerequisite, bare except→except Exception. Co-authored-by: bennytimz Salvaged from PR #8695. --- .../research/drug-discovery/SKILL.md | 226 ++++++++++++++++++ .../references/ADMET_REFERENCE.md | 66 +++++ .../drug-discovery/scripts/chembl_target.py | 53 ++++ .../drug-discovery/scripts/ro5_screen.py | 44 ++++ 4 files changed, 389 insertions(+) create mode 100644 optional-skills/research/drug-discovery/SKILL.md create mode 100644 optional-skills/research/drug-discovery/references/ADMET_REFERENCE.md create mode 100644 optional-skills/research/drug-discovery/scripts/chembl_target.py create mode 100644 optional-skills/research/drug-discovery/scripts/ro5_screen.py diff --git a/optional-skills/research/drug-discovery/SKILL.md b/optional-skills/research/drug-discovery/SKILL.md new file mode 100644 index 000000000..dc3bd3e7b --- /dev/null +++ b/optional-skills/research/drug-discovery/SKILL.md @@ -0,0 +1,226 @@ +--- +name: drug-discovery +description: > + Pharmaceutical research assistant for drug discovery workflows. Search + bioactive compounds on ChEMBL, calculate drug-likeness (Lipinski Ro5, QED, + TPSA, synthetic accessibility), look up drug-drug interactions via + OpenFDA, interpret ADMET profiles, and assist with lead optimization. + Use for medicinal chemistry questions, molecule property analysis, clinical + pharmacology, and open-science drug research. +version: 1.0.0 +author: bennytimz +license: MIT +metadata: + hermes: + tags: [science, chemistry, pharmacology, research, health] +prerequisites: + commands: [curl, python3] +--- + +# Drug Discovery & Pharmaceutical Research + +You are an expert pharmaceutical scientist and medicinal chemist with deep +knowledge of drug discovery, cheminformatics, and clinical pharmacology. +Use this skill for all pharma/chemistry research tasks. + +## Core Workflows + +### 1 — Bioactive Compound Search (ChEMBL) + +Search ChEMBL (the world's largest open bioactivity database) for compounds +by target, activity, or molecule name. No API key required. + +```bash +# Search compounds by target name (e.g. "EGFR", "COX-2", "ACE") +TARGET="$1" +ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$TARGET") +curl -s "https://www.ebi.ac.uk/chembl/api/data/target/search?q=${ENCODED}&format=json" \ + | python3 -c " +import json,sys +data=json.load(sys.stdin) +targets=data.get('targets',[])[:5] +for t in targets: + print(f\"ChEMBL ID : {t.get('target_chembl_id')}\") + print(f\"Name : {t.get('pref_name')}\") + print(f\"Type : {t.get('target_type')}\") + print() +" +``` + +```bash +# Get bioactivity data for a ChEMBL target ID +TARGET_ID="$1" # e.g. CHEMBL203 +curl -s "https://www.ebi.ac.uk/chembl/api/data/activity?target_chembl_id=${TARGET_ID}&pchembl_value__gte=6&limit=10&format=json" \ + | python3 -c " +import json,sys +data=json.load(sys.stdin) +acts=data.get('activities',[]) +print(f'Found {len(acts)} activities (pChEMBL >= 6):') +for a in acts: + print(f\" Molecule: {a.get('molecule_chembl_id')} | {a.get('standard_type')}: {a.get('standard_value')} {a.get('standard_units')} | pChEMBL: {a.get('pchembl_value')}\") +" +``` + +```bash +# Look up a specific molecule by ChEMBL ID +MOL_ID="$1" # e.g. CHEMBL25 (aspirin) +curl -s "https://www.ebi.ac.uk/chembl/api/data/molecule/${MOL_ID}?format=json" \ + | python3 -c " +import json,sys +m=json.load(sys.stdin) +props=m.get('molecule_properties',{}) or {} +print(f\"Name : {m.get('pref_name','N/A')}\") +print(f\"SMILES : {m.get('molecule_structures',{}).get('canonical_smiles','N/A') if m.get('molecule_structures') else 'N/A'}\") +print(f\"MW : {props.get('full_mwt','N/A')} Da\") +print(f\"LogP : {props.get('alogp','N/A')}\") +print(f\"HBD : {props.get('hbd','N/A')}\") +print(f\"HBA : {props.get('hba','N/A')}\") +print(f\"TPSA : {props.get('psa','N/A')} Ų\") +print(f\"Ro5 violations: {props.get('num_ro5_violations','N/A')}\") +print(f\"QED : {props.get('qed_weighted','N/A')}\") +" +``` + +### 2 — Drug-Likeness Calculation (Lipinski Ro5 + Veber) + +Assess any molecule against established oral bioavailability rules using +PubChem's free property API — no RDKit install needed. + +```bash +COMPOUND="$1" +ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$COMPOUND") +curl -s "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/${ENCODED}/property/MolecularWeight,XLogP,HBondDonorCount,HBondAcceptorCount,RotatableBondCount,TPSA,InChIKey/JSON" \ + | python3 -c " +import json,sys +data=json.load(sys.stdin) +props=data['PropertyTable']['Properties'][0] +mw = float(props.get('MolecularWeight', 0)) +logp = float(props.get('XLogP', 0)) +hbd = int(props.get('HBondDonorCount', 0)) +hba = int(props.get('HBondAcceptorCount', 0)) +rot = int(props.get('RotatableBondCount', 0)) +tpsa = float(props.get('TPSA', 0)) +print('=== Lipinski Rule of Five (Ro5) ===') +print(f' MW {mw:.1f} Da {\"✓\" if mw<=500 else \"✗ VIOLATION (>500)\"}') +print(f' LogP {logp:.2f} {\"✓\" if logp<=5 else \"✗ VIOLATION (>5)\"}') +print(f' HBD {hbd} {\"✓\" if hbd<=5 else \"✗ VIOLATION (>5)\"}') +print(f' HBA {hba} {\"✓\" if hba<=10 else \"✗ VIOLATION (>10)\"}') +viol = sum([mw>500, logp>5, hbd>5, hba>10]) +print(f' Violations: {viol}/4 {\"→ Likely orally bioavailable\" if viol<=1 else \"→ Poor oral bioavailability predicted\"}') +print() +print('=== Veber Oral Bioavailability Rules ===') +print(f' TPSA {tpsa:.1f} Ų {\"✓\" if tpsa<=140 else \"✗ VIOLATION (>140)\"}') +print(f' Rot. bonds {rot} {\"✓\" if rot<=10 else \"✗ VIOLATION (>10)\"}') +print(f' Both rules met: {\"Yes → good oral absorption predicted\" if tpsa<=140 and rot<=10 else \"No → reduced oral absorption\"}') +" +``` + +### 3 — Drug Interaction & Safety Lookup (OpenFDA) + +```bash +DRUG="$1" +ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$DRUG") +curl -s "https://api.fda.gov/drug/label.json?search=drug_interactions:\"${ENCODED}\"&limit=3" \ + | python3 -c " +import json,sys +data=json.load(sys.stdin) +results=data.get('results',[]) +if not results: + print('No interaction data found in FDA labels.') + sys.exit() +for r in results[:2]: + brand=r.get('openfda',{}).get('brand_name',['Unknown'])[0] + generic=r.get('openfda',{}).get('generic_name',['Unknown'])[0] + interactions=r.get('drug_interactions',['N/A'])[0] + print(f'--- {brand} ({generic}) ---') + print(interactions[:800]) + print() +" +``` + +```bash +DRUG="$1" +ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$DRUG") +curl -s "https://api.fda.gov/drug/event.json?search=patient.drug.medicinalproduct:\"${ENCODED}\"&count=patient.reaction.reactionmeddrapt.exact&limit=10" \ + | python3 -c " +import json,sys +data=json.load(sys.stdin) +results=data.get('results',[]) +if not results: + print('No adverse event data found.') + sys.exit() +print(f'Top adverse events reported:') +for r in results[:10]: + print(f\" {r['count']:>5}x {r['term']}\") +" +``` + +### 4 — PubChem Compound Search + +```bash +COMPOUND="$1" +ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$COMPOUND") +CID=$(curl -s "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/${ENCODED}/cids/TXT" | head -1 | tr -d '[:space:]') +echo "PubChem CID: $CID" +curl -s "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/${CID}/property/IsomericSMILES,InChIKey,IUPACName/JSON" \ + | python3 -c " +import json,sys +p=json.load(sys.stdin)['PropertyTable']['Properties'][0] +print(f\"IUPAC Name : {p.get('IUPACName','N/A')}\") +print(f\"SMILES : {p.get('IsomericSMILES','N/A')}\") +print(f\"InChIKey : {p.get('InChIKey','N/A')}\") +" +``` + +### 5 — Target & Disease Literature (OpenTargets) + +```bash +GENE="$1" +curl -s -X POST "https://api.platform.opentargets.org/api/v4/graphql" \ + -H "Content-Type: application/json" \ + -d "{\"query\":\"{ search(queryString: \\\"${GENE}\\\", entityNames: [\\\"target\\\"], page: {index: 0, size: 1}) { hits { id score object { ... on Target { id approvedSymbol approvedName associatedDiseases(page: {index: 0, size: 5}) { count rows { score disease { id name } } } } } } } }\"}" \ + | python3 -c " +import json,sys +data=json.load(sys.stdin) +hits=data.get('data',{}).get('search',{}).get('hits',[]) +if not hits: + print('Target not found.') + sys.exit() +obj=hits[0]['object'] +print(f\"Target: {obj.get('approvedSymbol')} — {obj.get('approvedName')}\") +assoc=obj.get('associatedDiseases',{}) +print(f\"Associated with {assoc.get('count',0)} diseases. Top associations:\") +for row in assoc.get('rows',[]): + print(f\" Score {row['score']:.3f} | {row['disease']['name']}\") +" +``` + +## Reasoning Guidelines + +When analysing drug-likeness or molecular properties, always: + +1. **State raw values first** — MW, LogP, HBD, HBA, TPSA, RotBonds +2. **Apply rule sets** — Ro5 (Lipinski), Veber, Ghose filter where relevant +3. **Flag liabilities** — metabolic hotspots, hERG risk, high TPSA for CNS penetration +4. **Suggest optimizations** — bioisosteric replacements, prodrug strategies, ring truncation +5. **Cite the source API** — ChEMBL, PubChem, OpenFDA, or OpenTargets + +For ADMET questions, reason through Absorption, Distribution, Metabolism, Excretion, Toxicity systematically. See references/ADMET_REFERENCE.md for detailed guidance. + +## Important Notes + +- All APIs are free, public, require no authentication +- ChEMBL rate limits: add sleep 1 between batch requests +- FDA data reflects reported adverse events, not necessarily causation +- Always recommend consulting a licensed pharmacist or physician for clinical decisions + +## Quick Reference + +| Task | API | Endpoint | +|------|-----|----------| +| Find target | ChEMBL | `/api/data/target/search?q=` | +| Get bioactivity | ChEMBL | `/api/data/activity?target_chembl_id=` | +| Molecule properties | PubChem | `/rest/pug/compound/name/{name}/property/` | +| Drug interactions | OpenFDA | `/drug/label.json?search=drug_interactions:` | +| Adverse events | OpenFDA | `/drug/event.json?search=...&count=reaction` | +| Gene-disease | OpenTargets | GraphQL POST `/api/v4/graphql` | diff --git a/optional-skills/research/drug-discovery/references/ADMET_REFERENCE.md b/optional-skills/research/drug-discovery/references/ADMET_REFERENCE.md new file mode 100644 index 000000000..92a5e9503 --- /dev/null +++ b/optional-skills/research/drug-discovery/references/ADMET_REFERENCE.md @@ -0,0 +1,66 @@ +# ADMET Reference Guide + +Comprehensive reference for Absorption, Distribution, Metabolism, Excretion, and Toxicity (ADMET) analysis in drug discovery. + +## Drug-Likeness Rule Sets + +### Lipinski's Rule of Five (Ro5) + +| Property | Threshold | +|----------|-----------| +| Molecular Weight (MW) | ≤ 500 Da | +| Lipophilicity (LogP) | ≤ 5 | +| H-Bond Donors (HBD) | ≤ 5 | +| H-Bond Acceptors (HBA) | ≤ 10 | + +Reference: Lipinski et al., Adv. Drug Deliv. Rev. 23, 3–25 (1997). + +### Veber's Oral Bioavailability Rules + +| Property | Threshold | +|----------|-----------| +| TPSA | ≤ 140 Ų | +| Rotatable Bonds | ≤ 10 | + +Reference: Veber et al., J. Med. Chem. 45, 2615–2623 (2002). + +### CNS Penetration (BBB) + +| Property | CNS-Optimal | +|----------|-------------| +| MW | ≤ 400 Da | +| LogP | 1–3 | +| TPSA | < 90 Ų | +| HBD | ≤ 3 | + +## CYP450 Metabolism + +| Isoform | % Drugs | Notable inhibitors | +|---------|---------|-------------------| +| CYP3A4 | ~50% | Grapefruit, ketoconazole | +| CYP2D6 | ~25% | Fluoxetine, paroxetine | +| CYP2C9 | ~15% | Fluconazole, amiodarone | +| CYP2C19 | ~10% | Omeprazole, fluoxetine | +| CYP1A2 | ~5% | Fluvoxamine, ciprofloxacin | + +## hERG Cardiac Toxicity Risk + +Structural alerts: basic nitrogen (pKa 7–9) + aromatic ring + hydrophobic moiety, LogP > 3.5 + basic amine. + +Mitigation: reduce basicity, introduce polar groups, break planarity. + +## Common Bioisosteric Replacements + +| Original | Bioisostere | Purpose | +|----------|-------------|---------| +| -COOH | -tetrazole, -SO₂NH₂ | Improve permeability | +| -OH (phenol) | -F, -CN | Reduce glucuronidation | +| Phenyl | Pyridine, thiophene | Reduce LogP | +| Ester | -CONHR | Reduce hydrolysis | + +## Key APIs + +- ChEMBL: https://www.ebi.ac.uk/chembl/api/data/ +- PubChem: https://pubchem.ncbi.nlm.nih.gov/rest/pug/ +- OpenFDA: https://api.fda.gov/drug/ +- OpenTargets GraphQL: https://api.platform.opentargets.org/api/v4/graphql diff --git a/optional-skills/research/drug-discovery/scripts/chembl_target.py b/optional-skills/research/drug-discovery/scripts/chembl_target.py new file mode 100644 index 000000000..1346b999a --- /dev/null +++ b/optional-skills/research/drug-discovery/scripts/chembl_target.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +""" +chembl_target.py — Search ChEMBL for a target and retrieve top active compounds. +Usage: python3 chembl_target.py "EGFR" --min-pchembl 7 --limit 20 +No external dependencies. +""" +import sys, json, time, argparse +import urllib.request, urllib.parse, urllib.error + +BASE = "https://www.ebi.ac.uk/chembl/api/data" + +def get(endpoint): + try: + req = urllib.request.Request(f"{BASE}{endpoint}", headers={"Accept":"application/json"}) + with urllib.request.urlopen(req, timeout=15) as r: + return json.loads(r.read()) + except Exception as e: + print(f"API error: {e}", file=sys.stderr); return None + +def main(): + parser = argparse.ArgumentParser(description="ChEMBL target → active compounds") + parser.add_argument("target") + parser.add_argument("--min-pchembl", type=float, default=6.0) + parser.add_argument("--limit", type=int, default=10) + args = parser.parse_args() + + enc = urllib.parse.quote(args.target) + data = get(f"/target/search?q={enc}&limit=5&format=json") + if not data or not data.get("targets"): + print("No targets found."); sys.exit(1) + + t = data["targets"][0] + tid = t.get("target_chembl_id","") + print(f"\nTarget: {t.get('pref_name')} ({tid})") + print(f"Type: {t.get('target_type')} | Organism: {t.get('organism','N/A')}") + print(f"\nFetching compounds with pChEMBL ≥ {args.min_pchembl}...\n") + + acts = get(f"/activity?target_chembl_id={tid}&pchembl_value__gte={args.min_pchembl}&assay_type=B&limit={args.limit}&order_by=-pchembl_value&format=json") + if not acts or not acts.get("activities"): + print("No activities found."); sys.exit(0) + + print(f"{'Molecule':<18} {'pChEMBL':>8} {'Type':<12} {'Value':<10} {'Units'}") + print("-"*65) + seen = set() + for a in acts["activities"]: + mid = a.get("molecule_chembl_id","N/A") + if mid in seen: continue + seen.add(mid) + print(f"{mid:<18} {str(a.get('pchembl_value','N/A')):>8} {str(a.get('standard_type','N/A')):<12} {str(a.get('standard_value','N/A')):<10} {a.get('standard_units','N/A')}") + time.sleep(0.1) + print(f"\nTotal: {len(seen)} unique molecules") + +if __name__ == "__main__": main() diff --git a/optional-skills/research/drug-discovery/scripts/ro5_screen.py b/optional-skills/research/drug-discovery/scripts/ro5_screen.py new file mode 100644 index 000000000..84e438fa1 --- /dev/null +++ b/optional-skills/research/drug-discovery/scripts/ro5_screen.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +""" +ro5_screen.py — Batch Lipinski Ro5 + Veber screening via PubChem API. +Usage: python3 ro5_screen.py aspirin ibuprofen paracetamol +No external dependencies beyond stdlib. +""" +import sys, json, time, argparse +import urllib.request, urllib.parse, urllib.error + +BASE = "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name" +PROPS = "MolecularWeight,XLogP,HBondDonorCount,HBondAcceptorCount,RotatableBondCount,TPSA" + +def fetch(name): + url = f"{BASE}/{urllib.parse.quote(name)}/property/{PROPS}/JSON" + try: + with urllib.request.urlopen(url, timeout=10) as r: + return json.loads(r.read())["PropertyTable"]["Properties"][0] + except Exception: + return None + +def check(p): + mw,logp,hbd,hba,rot,tpsa = float(p.get("MolecularWeight",0)),float(p.get("XLogP",0)),int(p.get("HBondDonorCount",0)),int(p.get("HBondAcceptorCount",0)),int(p.get("RotatableBondCount",0)),float(p.get("TPSA",0)) + v = sum([mw>500,logp>5,hbd>5,hba>10]) + return dict(mw=mw,logp=logp,hbd=hbd,hba=hba,rot=rot,tpsa=tpsa,violations=v,ro5=v<=1,veber=tpsa<=140 and rot<=10,ok=v<=1 and tpsa<=140 and rot<=10) + +def report(name, r): + if not r: print(f"✗ {name:30s} — not found"); return + s = "✓ PASS" if r["ok"] else "✗ FAIL" + flags = (f" [Ro5 violations:{r['violations']}]" if not r["ro5"] else "") + (" [Veber fail]" if not r["veber"] else "") + print(f"{s} {name:28s} MW={r['mw']:.0f} LogP={r['logp']:.2f} HBD={r['hbd']} HBA={r['hba']} TPSA={r['tpsa']:.0f} RotB={r['rot']}{flags}") + +def main(): + compounds = sys.stdin.read().splitlines() if len(sys.argv)<2 or sys.argv[1]=="-" else sys.argv[1:] + print(f"\n{'Status':<8} {'Compound':<30} Properties\n" + "-"*85) + passed = 0 + for name in compounds: + props = fetch(name.strip()) + result = check(props) if props else None + report(name.strip(), result) + if result and result["ok"]: passed += 1 + time.sleep(0.3) + print(f"\nSummary: {passed}/{len(compounds)} passed Ro5 + Veber.\n") + +if __name__ == "__main__": main()