Skill: cheminempharmaclaw/pcl-pharmacology-agent (version 2.0.1)

--- name: pharmaclaw-pharmacology-agent description: Pharmacology agent for ADME/PK profiling of drug candidates from SMILES. Computes drug-likeness (Lipinski Ro5, Veber rules), QED, SA Score, ADME predictions (BBB permeability, aqueous solubility, GI absorption, CYP3A4 inhibition, P-gp substrate, plasma protein binding), and PAINS alerts. Chains from chemistry-query for SMILES input. Triggers on pharmacology, ADME, PK/PD, drug likeness, Lipinski, absorption, distribution, metabolism, excretion, BBB, solubility, bioavailability, lead optimization, drug profiling. --- # Pharma Pharmacology Agent v2.0.0 ## Overview Predictive pharmacology profiling for drug candidates. Combines ADMETlab 3.0 ML predictions (when available) with comprehensive RDKit descriptor-based models. Provides full ADME assessment, toxicity risk, druglikeness scoring, and risk flagging — all from a SMILES string. **Key capabilities:** - **Drug-likeness:** Lipinski Rule of Five, Veber oral bioavailability rules - **Scores:** QED (Quantitative Estimate of Drug-likeness), SA Score (Synthetic Accessibility) - **ADME predictions:** BBB permeability, aqueous solubility (ESOL), GI absorption (Egan), CYP3A4 inhibition risk, P-glycoprotein substrate, plasma protein binding - **Safety:** PAINS (Pan-Assay Interference) filter alerts - **Risk assessment:** Automated flagging of pharmacological concerns - **Standard chain output:** JSON schema compatible with all downstream agents ## Quick Start ```bash # Profile a molecule from SMILES exec python scripts/chain_entry.py --input-json '{"smiles": "CC(=O)Oc1ccccc1C(=O)O", "context": "user"}' # Chain from chemistry-query output exec python scripts/chain_entry.py --input-json '{"smiles": "<canonical_smiles>", "context": "from_chemistry"}' ``` ## Scripts ### `scripts/chain_entry.py` Main entry point. Accepts JSON with `smiles` field, returns full pharmacology profile. **Input:** ```json {"smiles": "CN1C=NC2=C1C(=O)N(C(=O)N2C)C", "context": "user"} ``` **Output schema:** ```json { "agent": "pharma-pharmacology", "version": "1.1.0", "smiles": "<canonical>", "status": "success|error", "report": { "descriptors": {"mw": 194.08, "logp": -1.03, "tpsa": 61.82, "hbd": 0, "hba": 6, "rotb": 0, "arom_rings": 2, "heavy_atoms": 14, "mr": 51.2}, "lipinski": {"pass": true, "violations": 0, "details": {...}}, "veber": {"pass": true, "tpsa": {...}, "rotatable_bonds": {...}}, "qed": 0.5385, "sa_score": 2.3, "adme": { "bbb": {"prediction": "moderate", "confidence": "medium", "rationale": "..."}, "solubility": {"logS_estimate": -1.87, "class": "high", "rationale": "..."}, "gi_absorption": {"prediction": "high", "rationale": "..."}, "cyp3a4_inhibition": {"risk": "low", "rationale": "..."}, "pgp_substrate": {"prediction": "unlikely", "rationale": "..."}, "plasma_protein_binding": {"prediction": "moderate-low", "rationale": "..."} }, "pains": {"alert": false} }, "risks": [], "recommend_next": ["toxicology", "ip-expansion"], "confidence": 0.85, "warnings": [], "timestamp": "ISO8601" } ``` ## ADME Prediction Rules | Property | Method | Thresholds | |----------|--------|-----------| | BBB permeability | Clark's rules (TPSA/logP) | TPSA<60+logP 1-3 = high; TPSA<90 = moderate | | Solubility | ESOL approximation | logS > -2 high; > -4 moderate; else low | | GI absorption | Egan egg model | logP<5.6 and TPSA<131.6 = high | | CYP3A4 inhibition | Rule-based | logP>3 and MW>300 = high risk | | P-gp substrate | Rule-based | MW>400 and HBD>2 = likely | | Plasma protein binding | logP correlation | logP>3 = high (>90%) | ## Chaining This agent is designed to receive output from `chemistry-query`: ``` chemistry-query (name→SMILES+props) → pharma-pharmacology (ADME profile) → toxicology / ip-expansion ``` The `recommend_next` field always includes `["toxicology", "ip-expansion"]` for pipeline continuation. ## Tested With All features verified end-to-end with RDKit 2024.03+: | Molecule | MW | logP | Lipinski | Key Findings | |----------|-----|------|----------|-------------| | Caffeine | 194.08 | -1.03 | ✅ Pass (0 violations) | High solubility, moderate BBB, QED 0.54 | | Aspirin | 180.04 | 1.31 | ✅ Pass (0 violations) | Moderate solubility, SA 1.58 (easy), QED 0.55 | | Sotorasib | 560.23 | 4.48 | ✅ Pass (1 violation: MW) | Low solubility, CYP3A4 risk, high PPB | | Metformin | 129.10 | -1.03 | ✅ Pass (0 violations) | High solubility, low BBB, QED 0.25 | | Invalid SMILES | — | — | — | Graceful JSON error | | Empty input | — | — | — | Graceful JSON error | ## Error Handling - Invalid SMILES: Returns `status: "error"` with descriptive warning - Missing input: Clear error message requesting `smiles` or `name` - All errors produce valid JSON (never crashes) ### `scripts/admetlab3.py` Enhanced ADME/Tox predictor. Attempts ADMETlab 3.0 API first, falls back to comprehensive RDKit models. ```bash # Full ADME profile python scripts/admetlab3.py --smiles "CC(=O)Oc1ccccc1C(=O)O" # Specific categories python scripts/admetlab3.py --smiles "CN1C=NC2=C1C(=O)N(C(=O)N2C)C" --categories absorption,toxicity ``` **Output includes:** - **Physicochemical:** MW, LogP, TPSA, LogS (ESOL), solubility class, fraction CSP3, molar refractivity - **Absorption:** Lipinski, Veber, Egan, HIA, Caco-2 permeability, P-gp substrate, oral bioavailability - **Distribution:** BBB penetration (Clark model), plasma protein binding - **Metabolism:** CYP3A4 inhibition risk - **Toxicity:** hERG risk, Ames mutagenicity, DILI, structural alerts (nitro, aromatic amine) - **Druglikeness:** QED, SA Score, lead-like, drug-like classifications ## Resources - `references/api_reference.md` — API and methodology references ## Changelog **v2.0.0** (2026-02-18) - ADMETlab 3.0 integration (ML-based predictions, auto-fallback to RDKit) - Enhanced RDKit ADME: Caco-2 permeability, Egan model, HIA, hERG, Ames, DILI - Solubility via ESOL model - Lead-like / drug-like classification - Structural alerts: nitro groups, aromatic amines **v1.1.0** (2026-02-14) - Initial production release with full ADME profiling - Lipinski, Veber, QED, SA Score, PAINS - BBB, solubility, GI absorption, CYP3A4, P-gp, PPB predictions - Automated risk assessment - Standard chain output schema - Comprehensive error handling - End-to-end tested with diverse molecules

path: references/api_reference.md
content: # ADME Prediction Rules Reference

## Drug-Likeness Rules

### Lipinski Rule of Five (Ro5)
- MW < 500 Da
- logP < 5
- HBD (hydrogen bond donors) < 5
- HBA (hydrogen bond acceptors) < 10
- **Pass:** ≤1 violation. Ref: Lipinski et al., Adv Drug Deliv Rev, 2001.

### Veber Rules (Oral Bioavailability)
- TPSA ≤ 140 Å²
- Rotatable bonds ≤ 10
- Ref: Veber et al., J Med Chem, 2002.

## ADME Prediction Methods

### BBB Permeability (Clark's Rules)
- TPSA < 60 + logP 1–3 → high penetration
- TPSA < 90 → moderate
- TPSA ≥ 90 → low
- Ref: Clark, J Pharm Sci, 1999.

### Aqueous Solubility (ESOL Approximation)
- logS ≈ 0.16 − 0.63·logP − 0.0062·MW + 0.066·rotB − 0.74·aromRings
- logS > −2: high; −4 to −2: moderate; < −4: low
- Ref: Delaney, J Chem Inf Model, 2004.

### GI Absorption (Egan Egg Model)
- High if logP < 5.6 AND TPSA < 131.6
- Ref: Egan et al., J Med Chem, 2000.

### CYP3A4 Inhibition Risk
- Rule-based: logP > 3 AND MW > 300 → high risk
- Rationale: lipophilic, larger molecules more likely CYP substrates/inhibitors.

### P-glycoprotein Substrate
- MW > 400 AND HBD > 2 → likely substrate
- Ref: Polli et al., J Pharmacol Exp Ther, 2001.

### Plasma Protein Binding
- logP > 3 → high binding (>90%)
- Ref: Yamazaki & Kanaoka, J Pharm Sci, 2004.

## Scores

### QED (Quantitative Estimate of Drug-likeness)
- Range 0–1 (higher = more drug-like)
- Ref: Bickerton et al., Nat Chem, 2012.

### SA Score (Synthetic Accessibility)
- Range 1–10 (1 = easy, 10 = hard)
- Ref: Ertl & Schuffenhauer, J Cheminform, 2009.

### PAINS (Pan-Assay Interference Compounds)
- RDKit FilterCatalog with PAINS catalog
- Ref: Baell & Holloway, J Med Chem, 2010.


path: scripts/admetlab3.py
content: #!/usr/bin/env python3
"""ADMETlab 3.0 Integration — ML-based ADME/Tox predictions.

Queries the ADMETlab 3.0 API for ML-predicted ADME, toxicity, and druglikeness
properties. Falls back to RDKit rule-based predictions if API is unavailable.

ADMETlab 3.0: https://admetlab3.scbdd.com (Zhejiang University, free, no key required)

Usage:
    python admetlab3.py --smiles "CC(=O)OC1=CC=CC=C1C(=O)O"
    python admetlab3.py --smiles "CN1C=NC2=C1C(=O)N(C(=O)N2C)C" --categories absorption,toxicity
    python admetlab3.py --smiles "CC(C)CC1=CC=C(C=C1)C(C)C(=O)O" --all
"""

import argparse
import json
import sys
from datetime import datetime, timezone

try:
    import requests
except ImportError:
    print(json.dumps({"status": "error", "error": "requests not installed"}))
    sys.exit(1)

try:
    from rdkit import Chem
    from rdkit.Chem import Descriptors, rdMolDescriptors
    HAS_RDKIT = True
except ImportError:
    HAS_RDKIT = False

ADMETLAB_URL = "https://admetlab3.scbdd.com/service/evaluation"
TIMEOUT = 30

# ADMETlab 3.0 endpoint categories
ENDPOINTS = {
    "absorption": {
        "caco2": "Caco-2 permeability (intestinal absorption model)",
        "hia": "Human intestinal absorption",
        "pgp_inhibitor": "P-glycoprotein inhibitor",
        "pgp_substrate": "P-glycoprotein substrate",
        "bioavailability": "Oral bioavailability (F20%)",
    },
    "distribution": {
        "bbb": "Blood-brain barrier penetration",
        "ppb": "Plasma protein binding",
        "vd": "Volume of distribution",
    },
    "metabolism": {
        "cyp1a2_inhibitor": "CYP1A2 inhibitor",
        "cyp2c19_inhibitor": "CYP2C19 inhibitor",
        "cyp2c9_inhibitor": "CYP2C9 inhibitor",
        "cyp2d6_inhibitor": "CYP2D6 inhibitor",
        "cyp3a4_inhibitor": "CYP3A4 inhibitor",
        "cyp2d6_substrate": "CYP2D6 substrate",
        "cyp3a4_substrate": "CYP3A4 substrate",
    },
    "excretion": {
        "clearance": "Total clearance",
        "half_life": "Half-life",
    },
    "toxicity": {
        "herg": "hERG channel inhibition (cardiotoxicity)",
        "ames": "Ames mutagenicity",
        "dili": "Drug-induced liver injury",
        "skin_sensitization": "Skin sensitization",
        "carcinogenicity": "Carcinogenicity",
        "respiratory_toxicity": "Respiratory toxicity",
        "eye_irritation": "Eye irritation/corrosion",
    },
    "physicochemical": {
        "logp": "Lipophilicity (LogP)",
        "logs": "Aqueous solubility (LogS)",
        "logd": "Distribution coefficient (LogD7.4)",
        "pka": "Acid dissociation constant",
    },
}


def query_admetlab(smiles: str, categories: list[str] = None) -> dict:
    """Query ADMETlab 3.0 API for ADME/Tox predictions."""
    results = {}
    errors = []

    # Validate SMILES first
    if HAS_RDKIT:
        mol = Chem.MolFromSmiles(smiles)
        if mol is None:
            return {"status": "error", "error": f"Invalid SMILES: {smiles}"}
        smiles = Chem.MolToSmiles(mol)  # Canonicalize

    if categories is None:
        categories = list(ENDPOINTS.keys())

    # Try ADMETlab 3.0 API
    try:
        # ADMETlab 3.0 accepts SMILES via POST
        payload = {"smiles": smiles}
        resp = requests.post(
            f"{ADMETLAB_URL}/alogps",
            json=payload,
            timeout=TIMEOUT,
            headers={"Content-Type": "application/json"}
        )

        if resp.status_code == 200:
            data = resp.json()
            if data and not isinstance(data, str):
                results["admetlab3_raw"] = data
                results["source"] = "admetlab3"
        else:
            errors.append(f"ADMETlab API returned {resp.status_code}")
    except requests.exceptions.Timeout:
        errors.append("ADMETlab API timeout (30s)")
    except requests.exceptions.ConnectionError:
        errors.append("ADMETlab API unreachable")
    except Exception as e:
        errors.append(f"ADMETlab API error: {str(e)}")

    # Always compute RDKit-based predictions as baseline/fallback
    if HAS_RDKIT:
        rdkit_results = compute_rdkit_adme(smiles)
        if results.get("source") == "admetlab3":
            results["rdkit_baseline"] = rdkit_results
        else:
            results.update(rdkit_results)
            results["source"] = "rdkit_computed"
            if errors:
                results["api_notes"] = errors

    return results


def compute_rdkit_adme(smiles: str) -> dict:
    """Comprehensive RDKit-based ADME predictions (rule-based + descriptor-based)."""
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        return {"error": "Invalid SMILES"}

    mw = Descriptors.MolWt(mol)
    logp = Descriptors.MolLogP(mol)
    tpsa = Descriptors.TPSA(mol)
    hbd = Descriptors.NumHDonors(mol)
    hba = Descriptors.NumHAcceptors(mol)
    rotb = Descriptors.NumRotatableBonds(mol)
    arom_rings = rdMolDescriptors.CalcNumAromaticRings(mol)
    heavy_atoms = mol.GetNumHeavyAtoms()
    fsp3 = rdMolDescriptors.CalcFractionCSP3(mol)
    num_rings = rdMolDescriptors.CalcNumRings(mol)
    mr = Descriptors.MolMR(mol)

    # --- Absorption predictions ---
    # Lipinski Ro5
    lipinski_violations = sum([mw > 500, logp > 5, hbd > 5, hba > 10])
    lipinski_pass = lipinski_violations <= 1

    # Veber rules (oral bioavailability)
    veber_pass = rotb <= 10 and tpsa <= 140

    # Egan absorption model (logP vs TPSA)
    egan_pass = -1 < logp < 5.88 and tpsa < 131.6

    # HIA prediction (simple model: TPSA < 140 and logP > -1)
    hia_prediction = "High" if tpsa < 140 and logp > -1 else "Low"

    # Caco-2 prediction (Yazdanian model: logP-based estimate)
    # log Papp ≈ -5.47 + 0.54 * logP (simplified)
    caco2_log_papp = -5.47 + 0.54 * logp
    caco2_permeable = caco2_log_papp > -5.15  # threshold for high permeability

    # --- Distribution predictions ---
    # BBB penetration (Clark model: logP - (MW/100) + PSA correction)
    bbb_score = logp - 0.148 * (tpsa ** 0.5) - 0.00845 * mw
    bbb_penetrant = bbb_score > 0

    # PPB estimate (high LogP = high binding)
    ppb_prediction = "High (>90%)" if logp > 3 else "Moderate (50-90%)" if logp > 1 else "Low (<50%)"

    # --- Metabolism predictions ---
    # CYP3A4 inhibition risk (MW > 400, LogP > 3, aromatic rings)
    cyp3a4_risk = "High" if (mw > 400 and logp > 3) or arom_rings >= 3 else "Moderate" if mw > 300 and logp > 2 else "Low"

    # P-gp substrate prediction (MW > 400, HBD > 2)
    pgp_substrate = "Likely" if mw > 400 and hbd > 2 else "Unlikely"

    # --- Toxicity predictions ---
    # hERG risk (LogP > 3.7 and basic amine)
    has_basic_n = any(a.GetSymbol() == "N" and a.GetFormalCharge() >= 0
                      and not a.GetIsAromatic() for a in mol.GetAtoms())
    herg_risk = "High" if logp > 3.7 and has_basic_n else "Moderate" if logp > 3 else "Low"

    # Ames mutagenicity risk (aromatic amines, nitro groups)
    has_nitro = mol.HasSubstructMatch(Chem.MolFromSmarts("[N+](=O)[O-]")) if Chem.MolFromSmarts("[N+](=O)[O-]") else False
    has_aromatic_amine = mol.HasSubstructMatch(Chem.MolFromSmarts("[NH2]c")) if Chem.MolFromSmarts("[NH2]c") else False
    ames_risk = "High" if (has_nitro or has_aromatic_amine) else "Low"

    # DILI risk (reactive metabolite potential)
    dili_risk = "Moderate" if logp > 3 and mw > 300 else "Low"

    # --- Solubility ---
    # ESOL model: LogS = 0.16 - 0.63*logP - 0.0062*MW + 0.066*rotB - 0.74*aromRings
    logs_esol = 0.16 - 0.63 * logp - 0.0062 * mw + 0.066 * rotb - 0.74 * arom_rings
    solubility_class = "High" if logs_esol > -2 else "Moderate" if logs_esol > -4 else "Low" if logs_esol > -6 else "Very Low"

    # --- Druglikeness scores ---
    try:
        from rdkit.Chem import QED as QEDModule
        qed = round(QEDModule.qed(mol), 4)
    except:
        qed = None

    # Synthetic accessibility
    try:
        from rdkit.Chem import RDConfig
        import os
        sys.path.append(os.path.join(RDConfig.RDContribDir, 'SA_Score'))
        import sascorer
        sa_score = round(sascorer.calculateScore(mol), 2)
    except:
        sa_score = None

    return {
        "smiles": Chem.MolToSmiles(mol),
        "physicochemical": {
            "MW": round(mw, 2),
            "logP": round(logp, 2),
            "TPSA": round(tpsa, 2),
            "HBD": hbd,
            "HBA": hba,
            "rotatable_bonds": rotb,
            "aromatic_rings": arom_rings,
            "heavy_atoms": heavy_atoms,
            "fraction_csp3": round(fsp3, 3),
            "num_rings": num_rings,
            "molar_refractivity": round(mr, 2),
            "logS_ESOL": round(logs_esol, 2),
            "solubility_class": solubility_class,
        },
        "absorption": {
            "lipinski": {"pass": lipinski_pass, "violations": lipinski_violations},
            "veber": {"pass": veber_pass, "rotB_ok": rotb <= 10, "TPSA_ok": tpsa <= 140},
            "egan": {"pass": egan_pass},
            "HIA": hia_prediction,
            "caco2_log_papp": round(caco2_log_papp, 2),
            "caco2_permeable": caco2_permeable,
            "pgp_substrate": pgp_substrate,
            "oral_bioavailability": "Likely" if lipinski_pass and veber_pass else "Unlikely",
        },
        "distribution": {
            "BBB_score": round(bbb_score, 2),
            "BBB_penetrant": bbb_penetrant,
            "PPB": ppb_prediction,
        },
        "metabolism": {
            "CYP3A4_inhibition_risk": cyp3a4_risk,
        },
        "excretion": {
            "note": "Clearance/half-life require ML models (ADMETlab 3.0) for accurate prediction.",
        },
        "toxicity": {
            "hERG_risk": herg_risk,
            "ames_mutagenicity_risk": ames_risk,
            "DILI_risk": dili_risk,
            "alerts": {
                "nitro_group": has_nitro,
                "aromatic_amine": has_aromatic_amine,
            },
        },
        "druglikeness": {
            "QED": qed,
            "SA_score": sa_score,
            "lipinski_pass": lipinski_pass,
            "veber_pass": veber_pass,
            "lead_like": mw <= 350 and logp <= 3.5 and rotb <= 7,
            "drug_like": lipinski_pass and veber_pass and -2 < logp < 5,
        },
    }


def predict(smiles: str, categories: list[str] = None) -> dict:
    """Main prediction function."""
    results = query_admetlab(smiles, categories)

    return {
        "agent": "pharmacology",
        "version": "2.0.0",
        "action": "adme_predict",
        "smiles": smiles,
        "status": "success" if "error" not in results else "error",
        "predictions": results,
        "endpoints_available": ENDPOINTS,
        "recommend_next": ["toxicology", "ip-expansion", "literature"],
        "timestamp": datetime.now(timezone.utc).isoformat(),
    }


def main():
    parser = argparse.ArgumentParser(description="ADMETlab 3.0 + RDKit ADME Predictions")
    parser.add_argument("--smiles", required=True, help="SMILES string")
    parser.add_argument("--categories", help="Comma-separated: absorption,distribution,metabolism,excretion,toxicity,physicochemical")
    parser.add_argument("--all", action="store_true", help="Run all categories")
    args = parser.parse_args()

    categories = args.categories.split(",") if args.categories else None
    result = predict(args.smiles, categories)
    print(json.dumps(result, indent=2))


if __name__ == "__main__":
    main()


path: scripts/chain_entry.py
content: #!/usr/bin/env python3
"""Pharmacology Agent - ADME/PK profiling via RDKit descriptors and rule-based predictions."""
import argparse
import json
import sys
import os
from datetime import datetime, timezone

# === PharmaLab Dashboard Hook ===
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'pharmaclaw-lab-ui', 'scripts'))
try:
    from lab_hook import lab_update
except ImportError:
    def lab_update(*a, **kw): pass

try:
    from rdkit import Chem
    from rdkit.Chem import Descriptors, rdMolDescriptors, FilterCatalog
    from rdkit.Chem import QED as QEDModule
except ImportError as e:
    print(json.dumps({"error": f"RDKit import failed: {e}"}))
    sys.exit(1)

# Try SA Score import (may not be available in all RDKit installs)
try:
    from rdkit.Chem import RDConfig
    sys.path.append(os.path.join(RDConfig.RDContribDir, 'SA_Score'))
    import sascorer
    HAS_SASCORER = True
except Exception:
    HAS_SASCORER = False

# PAINS filter catalog
try:
    params = FilterCatalog.FilterCatalogParams()
    params.AddCatalog(FilterCatalog.FilterCatalogParams.FilterCatalogs.PAINS)
    PAINS_CATALOG = FilterCatalog.FilterCatalog(params)
    HAS_PAINS = True
except Exception:
    HAS_PAINS = False


def compute_descriptors(mol):
    """Compute core molecular descriptors."""
    mw = Descriptors.ExactMolWt(mol)
    logp = Descriptors.MolLogP(mol)
    tpsa = Descriptors.TPSA(mol)
    hbd = Descriptors.NumHDonors(mol)
    hba = Descriptors.NumHAcceptors(mol)
    rotb = Descriptors.NumRotatableBonds(mol)
    arom_rings = Descriptors.NumAromaticRings(mol)
    heavy_atoms = mol.GetNumHeavyAtoms()
    mr = Descriptors.MolMR(mol)
    return {
        "mw": round(mw, 2),
        "logp": round(logp, 4),
        "tpsa": round(tpsa, 2),
        "hbd": hbd,
        "hba": hba,
        "rotb": rotb,
        "arom_rings": arom_rings,
        "heavy_atoms": heavy_atoms,
        "mr": round(mr, 2),
    }


def lipinski(desc):
    """Lipinski Rule of Five."""
    violations = 0
    details = {}
    checks = [
        ("MW < 500", desc["mw"] < 500, desc["mw"]),
        ("logP < 5", desc["logp"] < 5, desc["logp"]),
        ("HBD < 5", desc["hbd"] < 5, desc["hbd"]),
        ("HBA < 10", desc["hba"] < 10, desc["hba"]),
    ]
    for label, passed, val in checks:
        details[label] = {"value": val, "pass": passed}
        if not passed:
            violations += 1
    return {
        "pass": violations <= 1,
        "violations": violations,
        "details": details,
    }


def veber(desc):
    """Veber rules for oral bioavailability."""
    tpsa_ok = desc["tpsa"] <= 140
    rotb_ok = desc["rotb"] <= 10
    return {
        "pass": tpsa_ok and rotb_ok,
        "tpsa": {"value": desc["tpsa"], "threshold": 140, "pass": tpsa_ok},
        "rotatable_bonds": {"value": desc["rotb"], "threshold": 10, "pass": rotb_ok},
    }


def compute_qed(mol):
    """Quantitative Estimate of Drug-likeness."""
    try:
        return round(QEDModule.qed(mol), 4)
    except Exception:
        return None


def compute_sa_score(mol):
    """Synthetic Accessibility Score (1=easy, 10=hard)."""
    if not HAS_SASCORER:
        return None
    try:
        return round(sascorer.calculateScore(mol), 2)
    except Exception:
        return None


def predict_adme(desc):
    """Rule-based ADME predictions."""
    adme = {}

    # BBB permeability (Clark's rules: TPSA<90 and logP 1-3 favorable)
    if desc["tpsa"] < 60 and 1 <= desc["logp"] <= 3:
        adme["bbb"] = {"prediction": "high", "confidence": "medium",
                       "rationale": f"TPSA={desc['tpsa']}<60, logP={desc['logp']} in 1-3 range"}
    elif desc["tpsa"] < 90:
        adme["bbb"] = {"prediction": "moderate", "confidence": "medium",
                       "rationale": f"TPSA={desc['tpsa']}<90"}
    else:
        adme["bbb"] = {"prediction": "low", "confidence": "medium",
                       "rationale": f"TPSA={desc['tpsa']}>=90, poor CNS penetration expected"}

    # Aqueous solubility (ESOL-like estimate: logS ≈ 0.16 - 0.63*logP - 0.0062*MW + 0.066*rotB - 0.74*aromRings)
    log_s = 0.16 - 0.63 * desc["logp"] - 0.0062 * desc["mw"] + 0.066 * desc["rotb"] - 0.74 * desc["arom_rings"]
    log_s = round(log_s, 2)
    if log_s > -2:
        sol_class = "high"
    elif log_s > -4:
        sol_class = "moderate"
    else:
        sol_class = "low"
    adme["solubility"] = {"logS_estimate": log_s, "class": sol_class,
                          "rationale": "ESOL-approximation from descriptors"}

    # GI absorption (Egan egg: logP<5.6 and TPSA<131.6)
    gi_pass = desc["logp"] < 5.6 and desc["tpsa"] < 131.6
    adme["gi_absorption"] = {"prediction": "high" if gi_pass else "low",
                             "rationale": "Egan egg model (logP/TPSA)"}

    # CYP3A4 inhibition risk (rule-based)
    cyp_risk = desc["logp"] > 3 and desc["mw"] > 300
    adme["cyp3a4_inhibition"] = {
        "risk": "high" if cyp_risk else "low",
        "rationale": f"logP={desc['logp']}>3 and MW={desc['mw']}>300" if cyp_risk else "Below risk thresholds"
    }

    # P-glycoprotein substrate prediction
    pgp_sub = desc["mw"] > 400 and desc["hbd"] > 2
    adme["pgp_substrate"] = {
        "prediction": "likely" if pgp_sub else "unlikely",
        "rationale": f"MW={desc['mw']}>400 and HBD={desc['hbd']}>2" if pgp_sub else "Below Pgp substrate thresholds"
    }

    # Plasma protein binding
    ppb_high = desc["logp"] > 3
    adme["plasma_protein_binding"] = {
        "prediction": "high (>90%)" if ppb_high else "moderate-low",
        "rationale": f"logP={desc['logp']}>3 suggests high PPB" if ppb_high else f"logP={desc['logp']}<=3"
    }

    return adme


def check_pains(mol):
    """Check for PAINS (Pan-Assay Interference Compounds) alerts."""
    if not HAS_PAINS:
        return {"checked": False, "reason": "PAINS catalog not available"}
    entry = PAINS_CATALOG.GetFirstMatch(mol)
    if entry:
        return {"alert": True, "pattern": entry.GetDescription()}
    return {"alert": False}


def assess_risks(desc, lipinski_result, adme, pains_result):
    """Compile risk factors."""
    risks = []
    if not lipinski_result["pass"]:
        risks.append(f"Lipinski violations: {lipinski_result['violations']}")
    if desc["mw"] > 600:
        risks.append(f"High MW ({desc['mw']}) - oral absorption concerns")
    if desc["logp"] > 5:
        risks.append(f"High logP ({desc['logp']}) - solubility/metabolism concerns")
    if desc["tpsa"] > 140:
        risks.append(f"High TPSA ({desc['tpsa']}) - permeability concerns")
    if adme.get("solubility", {}).get("class") == "low":
        risks.append("Low predicted aqueous solubility")
    if adme.get("cyp3a4_inhibition", {}).get("risk") == "high":
        risks.append("CYP3A4 inhibition risk - potential drug-drug interactions")
    if adme.get("pgp_substrate", {}).get("prediction") == "likely":
        risks.append("Likely P-gp substrate - may affect oral absorption")
    if isinstance(pains_result, dict) and pains_result.get("alert"):
        risks.append(f"PAINS alert: {pains_result.get('pattern', 'unknown')}")
    return risks


def main():
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument('--input-json')
    args = parser.parse_args()

    if args.input_json:
        input_data = json.loads(args.input_json)
    else:
        input_data = json.load(sys.stdin)

    smiles = input_data.get('smiles', '')
    canonical_smiles = ''

    try:
        if not smiles:
            name = input_data.get('name', '')
            if not name:
                raise ValueError('Require "smiles" or "name" in input')
            # Try to resolve via PubChem (optional - caller should provide SMILES)
            raise ValueError(f'No SMILES provided. Chain from chemistry-query first, or provide "smiles" directly.')

        mol = Chem.MolFromSmiles(smiles)
        if mol is None:
            raise ValueError(f'Invalid SMILES: {smiles}')
        canonical_smiles = Chem.MolToSmiles(mol, canonical=True, isomericSmiles=True)
        lab_update("pharmacology", "analyzing", f"ADME profiling {canonical_smiles[:40]}...",
                   smiles=canonical_smiles)

        # Core descriptors
        desc = compute_descriptors(mol)

        # Drug-likeness rules
        lip = lipinski(desc)
        veb = veber(desc)
        qed_val = compute_qed(mol)
        sa_score = compute_sa_score(mol)

        # ADME predictions
        adme = predict_adme(desc)

        # PAINS check
        pains = check_pains(mol)

        # Risk assessment
        risks = assess_risks(desc, lip, adme, pains)

        report = {
            "descriptors": desc,
            "lipinski": lip,
            "veber": veb,
            "qed": qed_val,
            "sa_score": sa_score,
            "adme": adme,
            "pains": pains,
        }

        lab_update("pharmacology", "idle", "ADME profile complete")

        output = {
            "agent": "pharma-pharmacology",
            "version": "1.1.0",
            "smiles": canonical_smiles,
            "status": "success",
            "report": report,
            "risks": risks,
            "recommend_next": ["toxicology", "ip-expansion"],
            "confidence": 0.85,
            "warnings": [],
            "timestamp": datetime.now(timezone.utc).isoformat(),
        }

    except Exception as e:
        lab_update("pharmacology", "error", f"Failed: {str(e)[:60]}")
        output = {
            "agent": "pharma-pharmacology",
            "version": "1.1.0",
            "smiles": canonical_smiles,
            "status": "error",
            "report": {},
            "risks": [],
            "recommend_next": ["toxicology", "ip-expansion"],
            "confidence": 0.0,
            "warnings": [str(e)],
            "timestamp": datetime.now(timezone.utc).isoformat(),
        }

    print(json.dumps(output, indent=2, default=str))


if __name__ == '__main__':
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)