Skill: cheminem/pharmaclaw-market-intel-agent (version 1.0.0)

--- name: pharmaclaw-market-intel-agent description: | Fetches and analyzes FAERS (FDA Adverse Event Reporting System) data from openFDA API. Supports drug names and SMILES (resolves via PubChem). Generates: events list, yearly trends (counts), top reactions/outcomes as JSON + matplotlib bar chart PNGs. triggers: ['faers', 'adverse event', 'safety report', 'drug side effect', 'post-market surveillance', 'reaction trend', 'clinical trial', 'clinicaltrials', 'trial pipeline', 'recruiting trial'] --- # Pharma Market Intel Agent - FAERS Query Skill ## Overview Query real-world post-market safety data for drugs. Useful for market intel on safety profiles, emerging risks, competitor analysis. Key outputs: - JSON summaries (trends, top reactions/outcomes) - PNG bar charts (yearly reports, top 10 reactions/outcomes) - Sample recent events **Rate limits**: openFDA ~240 req/min. Counts are fast (no full data). ## Chemistry-Query Structure Parse user queries into this model for standardized chaining: ```python from dataclasses import dataclass from typing import List, Optional @dataclass class ChemistryQuery: drug: str # Drug name or SMILES query_type: str = 'faers' # 'faers', 'pubchem', etc. metrics: Optional[List[str]] = None # ['yearly_trends', 'top_reactions', 'top_outcomes', 'events'] limit_events: int = 20 ``` Example: ```json { \"drug\": \"aspirin\", // or \"CC(=O)OC1=CC=CC=C1C(=O)O\" \"query_type\": \"faers\", \"metrics\": [\"yearly_trends\", \"top_reactions\"] } ``` ## Quick Start / Workflows ### 1. Basic Query (All Metrics) ``` exec skills/pharma-market-intel-agent/scripts/query_faers.py --drug aspirin --output ./aspirin_faers ``` Generates: - aspirin_faers/aspirin_summary.json - *.png plots - Recent events JSON ### 2. SMILES Input ``` exec ... --drug \"CC(=O)OC1=CC=CC=C1C(=O)O\" # Aspirin SMILES ``` Auto-resolves to name via PubChem. ### 3. Custom Limit ``` exec ... --drug ozempic --limit-events 50 --output ozempic_analysis ``` ## Chaining Examples - **With chemistry-query**: Resolve/validate SMILES first, then FAERS. - **pharma-tox-agent**: Feed top reactions for tox prediction. - **pharma-ip-expansion-agent**: Check safety for IP expansion targets. - **traction-agent**: Market risk scoring from FAERS trends. ``` # Agent workflow: 1. Parse ChemistryQuery 2. Resolve SMILES if needed (pubchempy or query_faers handles) 3. Run query_faers.py 4. Read PNGs/JSONs into response 5. Chain if metrics require ``` --- ## ClinicalTrials.gov Integration Query clinical trial data from ClinicalTrials.gov API v2. Search by drug, condition, phase, and status. No API key needed. ### Quick Start ```bash # Search by drug exec skills/pharma-market-intel-agent/scripts/query_trials.py --drug "sotorasib" --output ./sotorasib_trials # Search by condition + filters exec ... --condition "breast cancer" --phase PHASE3 --status RECRUITING --limit 10 --output ./bc_trials # Search by both exec ... --drug "pembrolizumab" --condition "NSCLC" --output ./pembro_trials # SMILES input (auto-resolves via PubChem) exec ... --drug "CC(=O)OC1=CC=CC=C1C(=O)O" --output ./aspirin_trials ``` ### Outputs - `{drug}_trials_summary.json` — Full structured summary with trials list and aggregate stats - `{drug}_trials_by_phase.png` — Bar chart by phase - `{drug}_trials_by_status.png` — Bar chart by status - `{drug}_trials_timeline.png` — Timeline of trial start dates ### JSON Summary Structure ```json { "drug": "sotorasib", "total_found": 45, "trials": [{"nct_id": "NCT...", "title": "...", "phase": "PHASE3", "sponsor": "Amgen", ...}], "stats": {"by_phase": {...}, "by_status": {...}, "top_sponsors": [...], "top_conditions": [...]} } ``` ### Chaining Examples - **Chemistry Query → Market Intel**: Resolve SMILES, then query trials for competitive landscape - **FAERS + Trials**: Run both scripts for a drug to get safety profile + development pipeline - **chain_entry.py**: Use `--metrics trials` or `--metrics faers,trials` to run both in one call --- ## References - [faers_fields.md](./references/faers_fields.md): Key FAERS fields & search syntax - [clinicaltrials_fields.md](./references/clinicaltrials_fields.md): ClinicalTrials.gov API fields & enums - [openFDA Drug Event API](https://open.fda.gov/apis/drug/event/) - [ClinicalTrials.gov API v2](https://clinicaltrials.gov/data-api/api) - [PubChem PUG REST](https://pubchem.ncbi.nlm.nih.gov/docs/pug-rest) ## Resources - **scripts/query_faers.py**: FAERS query executable - **scripts/query_trials.py**: ClinicalTrials.gov query executable - **scripts/chain_entry.py**: Unified entry point (faers + trials) - **assets/**: Store generated PNGs here for reuse

path: assets/example_asset.txt
content: # Example Asset File

This placeholder represents where asset files would be stored.
Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed.

Asset files are NOT intended to be loaded into context, but rather used within
the output Codex produces.

Example asset files from other skills:
- Brand guidelines: logo.png, slides_template.pptx
- Frontend builder: hello-world/ directory with HTML/React boilerplate
- Typography: custom-font.ttf, font-family.woff2
- Data: sample_data.csv, test_dataset.json

## Common Asset Types

- Templates: .pptx, .docx, boilerplate directories
- Images: .png, .jpg, .svg, .gif
- Fonts: .ttf, .otf, .woff, .woff2
- Boilerplate code: Project directories, starter files
- Icons: .ico, .svg
- Data files: .csv, .json, .xml, .yaml

Note: This is a text placeholder. Actual assets can be any file type.


path: package_skill.py
content: #!/usr/bin/env python3
import os
import tarfile
import shutil

def main(skill_dir='.'):
    skill_dir = os.path.abspath(skill_dir)
    skill_name = os.path.basename(skill_dir)
    skill_path = os.path.dirname(skill_dir)
    skill_file = os.path.join(skill_path, f'{skill_name}.skill')
    
    # Clean temp files
    if os.path.exists('__pycache__'):
        shutil.rmtree('__pycache__')
    
    with tarfile.open(skill_file, 'w:gz') as tar:
        tar.add(skill_dir, arcname=skill_name, recursive=True)
    
    print(f'Successfully packaged: {skill_file}')
    print(f'Size: {os.path.getsize(skill_file) / 1024:.1f} KB')

if __name__ == '__main__':
    main()


path: references/api_reference.md
content: # Reference Documentation for Pharma Market Intel Agent

This is a placeholder for detailed reference documentation.
Replace with actual reference content or delete if not needed.

Example real reference docs from other skills:
- product-management/references/communication.md - Comprehensive guide for status updates
- product-management/references/context_building.md - Deep-dive on gathering context
- bigquery/references/ - API references and query examples

## When Reference Docs Are Useful

Reference docs are ideal for:
- Comprehensive API documentation
- Detailed workflow guides
- Complex multi-step processes
- Information too lengthy for main SKILL.md
- Content that's only needed for specific use cases

## Structure Suggestions

### API Reference Example
- Overview
- Authentication
- Endpoints with examples
- Error codes
- Rate limits

### Workflow Guide Example
- Prerequisites
- Step-by-step instructions
- Common patterns
- Troubleshooting
- Best practices


path: references/clinicaltrials_fields.md
content: # ClinicalTrials.gov API v2 Reference

## Base URL
```
https://clinicaltrials.gov/api/v2/studies
```
No API key required.

## Key Query Parameters

| Parameter | Description | Example |
|-----------|-------------|---------|
| `query.term` | General search term | `sotorasib` |
| `query.cond` | Condition/disease | `lung cancer` |
| `query.intr` | Intervention/drug | `sotorasib` |
| `filter.overallStatus` | Trial status filter | `RECRUITING` |
| `filter.phase` | Phase filter | `PHASE3` |
| `pageSize` | Results per page (max 100) | `20` |
| `pageToken` | Pagination token | (from response) |
| `format` | Response format | `json` |

## Status Enums
- `RECRUITING` — Currently recruiting participants
- `COMPLETED` — Study completed
- `ACTIVE_NOT_RECRUITING` — Active, no longer recruiting
- `NOT_YET_RECRUITING` — Not yet started recruiting
- `TERMINATED` — Stopped early
- `WITHDRAWN` — Withdrawn before enrollment
- `ENROLLING_BY_INVITATION` — Invitation only
- `SUSPENDED` — Temporarily halted
- `UNKNOWN` — Status unknown

## Phase Enums
- `EARLY_PHASE1`
- `PHASE1`
- `PHASE2`
- `PHASE3`
- `PHASE4`
- `NA` — Not applicable

## Key Response Fields

```
protocolSection.identificationModule
  .nctId          — NCT identifier
  .briefTitle     — Short title
  .officialTitle  — Full title

protocolSection.statusModule
  .overallStatus          — Current status
  .startDateStruct.date   — Start date
  .completionDateStruct.date — Expected completion

protocolSection.designModule
  .studyType              — INTERVENTIONAL, OBSERVATIONAL
  .phases                 — ["PHASE1", "PHASE2"]
  .enrollmentInfo.count   — Target enrollment

protocolSection.conditionsModule
  .conditions             — List of conditions

protocolSection.armsInterventionsModule
  .interventions[].name   — Intervention names
  .interventions[].type   — DRUG, BIOLOGICAL, etc.

protocolSection.sponsorCollaboratorsModule
  .leadSponsor.name       — Lead sponsor name
```

## Pagination
Response includes `nextPageToken` for pagination. Loop until no token or desired count reached.

## Rate Limits
No formal rate limit documented, but be respectful (~1 req/sec for bulk queries).


path: references/faers_fields.md
content: # FAERS Fields Reference (openFDA Drug Event API)

## Core Report Fields
- `safetyreportid`: Unique ID
- `receivedate`: Report received date (YYYYMMDD)
- `receiptdate`: Initial receipt date
- `serious`: 1 if serious
- `seriousnessdeath`: 1 if death
- `transmissiondate`: Transmission date

## Primary Source
- `primarysource.reportercountry`: Country
- `primarysource.qualification`: Qualification code

## Patient
- `patient.patientonsetage`: Age at onset
- `patient.patientonsetageunit`: Unit (801=year)
- `patientsex`: 1=Male, 2=Female
- `patientdeath.patientdeathdate`: Death date

## Reactions (array)
- `patient.reaction.reactionmeddrapt`: Preferred Term (PT)
- `patient.reaction.reactionmeddrasoc`: System Organ Class (SOC)

## Drugs (array)
- `patient.drug.medicinalproduct`: Generic name
- `patient.drug.drugcharacterization`: 1=Suspect Single, 2=Concomitant
- `patient.drug.openfda.brand_name`: Brand name
- `patient.drug.openfda.manufacturer_name`
- `patient.drug.drugadministrationroute`: Route code
- `patient.drug.drugindication`: Indication

## Outcomes (array)
- `patient.outcome.result`: 1=Fatal, 2=Hospitalized, etc.

## Search Examples
- Drug: `patient.drug.medicinalproduct:\"aspirin\"`
- Brand: `patient.drug.openfda.brand_name:\"Bayer Aspirin\"`
- Reaction: `patient.reaction.reactionmeddrapt:\"NAUSEA\"`
- Year: `receivedate:[20040101 TO 20260101]`

## Count Endpoints
Use `&count=receivedate` for yearly trends (parse year from date).
`&count=patient.reaction.reactionmeddrapt` for top reactions.

More fields: https://open.fda.gov/apis/drug/event/

path: scripts/chain_entry.py
content: #!/usr/bin/env python3
"""Unified entry point for Pharma Market Intel: FAERS + ClinicalTrials.gov.

Usage:
    python chain_entry.py --drug "sotorasib" --metrics faers,trials --output ./output
    python chain_entry.py --drug "aspirin" --metrics trials --condition "pain" --output ./output
"""
import argparse
import json
import os
import subprocess
import sys

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PYTHON = sys.executable


def run_faers(drug, output, limit_events=20):
    """Run query_faers.py and return success bool."""
    cmd = [PYTHON, os.path.join(SCRIPT_DIR, "query_faers.py"),
           "--drug", drug, "--output", output,
           "--limit-events", str(limit_events)]
    print(f"[chain] Running FAERS query...", file=sys.stderr)
    result = subprocess.run(cmd, capture_output=True, text=True)
    print(result.stderr, file=sys.stderr, end='')
    print(result.stdout, end='')
    return result.returncode == 0


def run_trials(drug=None, condition=None, status=None, phase=None, limit=20, output="."):
    """Run query_trials.py and return success bool."""
    cmd = [PYTHON, os.path.join(SCRIPT_DIR, "query_trials.py"), "--output", output]
    if drug:
        cmd.extend(["--drug", drug])
    if condition:
        cmd.extend(["--condition", condition])
    if status:
        cmd.extend(["--status", status])
    if phase:
        cmd.extend(["--phase", phase])
    cmd.extend(["--limit", str(limit)])
    print(f"[chain] Running ClinicalTrials.gov query...", file=sys.stderr)
    result = subprocess.run(cmd, capture_output=True, text=True)
    print(result.stderr, file=sys.stderr, end='')
    print(result.stdout, end='')
    return result.returncode == 0


def main():
    parser = argparse.ArgumentParser(description="Pharma Market Intel unified entry point.")
    parser.add_argument("--drug", required=True, help="Drug name or SMILES")
    parser.add_argument("--metrics", default="faers,trials",
                        help="Comma-separated: faers, trials, clinical_trials (default: faers,trials)")
    parser.add_argument("--condition", help="Condition for trials search")
    parser.add_argument("--status", help="Trial status filter")
    parser.add_argument("--phase", help="Trial phase filter")
    parser.add_argument("--limit", type=int, default=20, help="Limit for trials/events")
    parser.add_argument("--output", default="./market_intel_output", help="Output directory")
    args = parser.parse_args()

    os.makedirs(args.output, exist_ok=True)
    metrics = [m.strip().lower() for m in args.metrics.split(",")]

    results = {"drug": args.drug, "metrics_requested": metrics, "outputs": {}}

    if "faers" in metrics:
        faers_dir = os.path.join(args.output, "faers")
        os.makedirs(faers_dir, exist_ok=True)
        ok = run_faers(args.drug, faers_dir, args.limit)
        results["outputs"]["faers"] = {"success": ok, "directory": faers_dir}

    if "trials" in metrics or "clinical_trials" in metrics:
        trials_dir = os.path.join(args.output, "trials")
        os.makedirs(trials_dir, exist_ok=True)
        ok = run_trials(
            drug=args.drug, condition=args.condition,
            status=args.status, phase=args.phase,
            limit=args.limit, output=trials_dir
        )
        results["outputs"]["trials"] = {"success": ok, "directory": trials_dir}

    # Write combined manifest
    manifest_path = os.path.join(args.output, "chain_manifest.json")
    with open(manifest_path, 'w') as f:
        json.dump(results, f, indent=2)
    print(f"\n[chain] Complete. Manifest: {manifest_path}")


if __name__ == "__main__":
    main()


path: scripts/example.py
content: #!/usr/bin/env python3
"""
Example helper script for pharma-market-intel-agent

This is a placeholder script that can be executed directly.
Replace with actual implementation or delete if not needed.

Example real scripts from other skills:
- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields
- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images
"""

def main():
    print("This is an example script for pharma-market-intel-agent")
    # TODO: Add actual script logic here
    # This could be data processing, file conversion, API calls, etc.

if __name__ == "__main__":
    main()


path: scripts/query_faers.py
content: #!/usr/bin/env python3
import requests
import pandas as pd
import matplotlib.pyplot as plt
import argparse
import json
import os
import re
import sys

def get_drug_name_from_smiles(smiles):
    """Resolve SMILES to primary drug name via PubChem API."""
    try:
        # Get CID from SMILES (strict canonical)
        cid_url = f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/smiles/{smiles}/cids/JSON"
        cid_resp = requests.get(cid_url, timeout=10)
        if cid_resp.status_code != 200:
            return None
        cids = cid_resp.json().get('IdentifierList', {}).get('CID', [])
        if not cids:
            return None
        cid = cids[0]
        
        # Get synonyms
        syn_url = f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/{cid}/synonyms/JSON"
        syn_resp = requests.get(syn_url, timeout=10)
        if syn_resp.status_code != 200:
            return None
        info = syn_resp.json().get('InformationList', {}).get('Information', [{}])[0]
        synonyms = info.get('Synonym', [])
        if synonyms:
            # Prefer non-IUPAC name if available
            for syn in synonyms:
                if not syn.startswith('InChI=') and len(syn) < 50:
                    return syn
            return synonyms[0]
    except:
        pass
    return None

def query_faers(drug_query, count_field=None, limit=100):
    """Query openFDA FAERS API."""
    base_url = "https://api.fda.gov/drug/event.json"
    params = {
        'search': f'patient.drug.medicinalproduct:"{drug_query}"',
        'limit': limit
    }
    if count_field:
        params['count'] = count_field
        params['limit'] = 0  # No results for count
    resp = requests.get(base_url, params=params, timeout=30)
    resp.raise_for_status()
    return resp.json()

def parse_year(date_str):
    if date_str and len(date_str) >= 4:
        return int(date_str[:4])
    return None

def main():
    parser = argparse.ArgumentParser(description='Query FAERS for drug adverse events, generate trends and plots.')
    parser.add_argument('--drug', required=True, help='Drug name or SMILES string')
    parser.add_argument('--output', default='./faers_output', help='Output directory')
    parser.add_argument('--limit-events', type=int, default=20, help='Max events to list')
    args = parser.parse_args()

    os.makedirs(args.output, exist_ok=True)
    drug_name = args.drug.strip()

    # Resolve SMILES if looks like one
    if re.search(r'^[A-Za-z0-9#\(\)=\[\]@+\-%/.]+$', drug_name) and len(drug_name) > 3 and not drug_name.isalpha():
        print(f"Resolving SMILES '{drug_name}' to drug name...", file=sys.stderr)
        resolved = get_drug_name_from_smiles(drug_name)
        if resolved:
            drug_name = resolved
            print(f"Resolved to: {drug_name}", file=sys.stderr)
        else:
            print(f"Failed to resolve SMILES '{args.drug}', using as-is.", file=sys.stderr)

    print(f"Querying FAERS for drug: {drug_name}", file=sys.stderr)

    results = {}

    # 1. Yearly trends
    try:
        yearly_data = query_faers(drug_name, count_field='receivedate', limit=100)
        if 'results' in yearly_data:
            df_year = pd.DataFrame(yearly_data['results'])
            df_year['year'] = df_year['term'].apply(parse_year)
            yearly_counts = df_year.dropna(subset=['year']).groupby('year')['count'].sum().reset_index()
            yearly_counts.to_json(os.path.join(args.output, f'{drug_name}_yearly_trends.json'), orient='records', indent=2)
            results['yearly_trends'] = yearly_counts.to_dict('records')

            plt.figure(figsize=(10, 6))
            plt.bar(yearly_counts['year'], yearly_counts['count'])
            plt.title(f'FAERS Yearly Report Counts: {drug_name}')
            plt.xlabel('Year')
            plt.ylabel('Number of Reports')
            plt.xticks(rotation=45)
            plt.tight_layout()
            plt.savefig(os.path.join(args.output, f'{drug_name}_yearly_trends.png'), dpi=150, bbox_inches='tight')
            plt.close()
    except Exception as e:
        print(f"Error generating yearly trends: {e}", file=sys.stderr)

    # 2. Top reactions
    try:
        reactions_data = query_faers(drug_name, count_field='patient.reaction.reactionmeddrapt', limit=25)
        if 'results' in reactions_data:
            df_react = pd.DataFrame(reactions_data['results'].head(10))
            df_react.to_json(os.path.join(args.output, f'{drug_name}_top_reactions.json'), orient='records', indent=2)
            results['top_reactions'] = df_react.to_dict('records')

            plt.figure(figsize=(10, 8))
            plt.barh(range(len(df_react)), df_react['count'])
            plt.yticks(range(len(df_react)), df_react['term'])
            plt.title(f'Top 10 Reactions: {drug_name}')
            plt.xlabel('Number of Reports')
            plt.tight_layout()
            plt.savefig(os.path.join(args.output, f'{drug_name}_top_reactions.png'), dpi=150, bbox_inches='tight')
            plt.close()
    except Exception as e:
        print(f"Error generating top reactions: {e}", file=sys.stderr)

    # 3. Top outcomes
    try:
        outcomes_data = query_faers(drug_name, count_field='patient.outcome.result', limit=25)
        if 'results' in outcomes_data:
            df_out = pd.DataFrame(outcomes_data['results'].head(10))
            df_out.to_json(os.path.join(args.output, f'{drug_name}_top_outcomes.json'), orient='records', indent=2)
            results['top_outcomes'] = df_out.to_dict('records')

            plt.figure(figsize=(10, 8))
            plt.barh(range(len(df_out)), df_out['count'])
            plt.yticks(range(len(df_out)), df_out['term'])
            plt.title(f'Top 10 Outcomes: {drug_name}')
            plt.xlabel('Number of Reports')
            plt.tight_layout()
            plt.savefig(os.path.join(args.output, f'{drug_name}_top_outcomes.png'), dpi=150, bbox_inches='tight')
            plt.close()
    except Exception as e:
        print(f"Error generating top outcomes: {e}", file=sys.stderr)

    # 4. Recent events list (limited)
    try:
        events_data = query_faers(drug_name, limit=args.limit_events)
        events_path = os.path.join(args.output, f'{drug_name}_recent_events.json')
        with open(events_path, 'w') as f:
            json.dump(events_data, f, indent=2)
        results['recent_events_count'] = len(events_data.get('results', []))
    except Exception as e:
        print(f"Error fetching events list: {e}", file=sys.stderr)

    # Summary JSON
    summary_path = os.path.join(args.output, f'{drug_name}_summary.json')
    with open(summary_path, 'w') as f:
        json.dump(results, f, indent=2)

    print(f"Analysis complete. Files saved to: {args.output}/", file=sys.stdout)
    print(f"Summary: {summary_path}", file=sys.stdout)

if __name__ == '__main__':
    main()


path: scripts/query_trials.py
content: #!/usr/bin/env python3
"""Query ClinicalTrials.gov API v2 for drug/condition clinical trial data.

Generates JSON summaries and matplotlib visualizations.
"""
import argparse
import json
import os
import re
import sys
from collections import Counter
from datetime import datetime

import requests
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

API_BASE = "https://clinicaltrials.gov/api/v2/studies"

VALID_STATUSES = [
    "RECRUITING", "COMPLETED", "ACTIVE_NOT_RECRUITING",
    "NOT_YET_RECRUITING", "TERMINATED", "WITHDRAWN",
    "ENROLLING_BY_INVITATION", "SUSPENDED", "UNKNOWN"
]
VALID_PHASES = ["EARLY_PHASE1", "PHASE1", "PHASE2", "PHASE3", "PHASE4", "NA"]


def resolve_smiles_to_name(smiles):
    """Resolve SMILES to drug name via PubChem."""
    try:
        import urllib.parse
        encoded = urllib.parse.quote(smiles, safe='')
        url = f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/smiles/{encoded}/synonyms/JSON"
        resp = requests.get(url, timeout=15)
        if resp.status_code != 200:
            return None
        synonyms = (resp.json()
                    .get('InformationList', {})
                    .get('Information', [{}])[0]
                    .get('Synonym', []))
        for syn in synonyms:
            if not syn.startswith('InChI=') and len(syn) < 50 and syn.isascii():
                return syn
        return synonyms[0] if synonyms else None
    except Exception:
        return None


def is_smiles(s):
    """Heuristic: looks like SMILES rather than a drug name."""
    return bool(re.search(r'[=#\[\]\\/@]', s)) or (not s.isalpha() and len(s) > 5)


def fetch_trials(drug=None, condition=None, status=None, phase=None, limit=20):
    """Fetch trials from ClinicalTrials.gov API v2."""
    params = {
        "pageSize": min(limit, 100),
        "format": "json",
    }

    # Build query
    if drug:
        params["query.intr"] = drug
    if condition:
        params["query.cond"] = condition
    if not drug and not condition:
        raise ValueError("Must specify --drug or --condition (or both)")

    if status:
        s = status.upper()
        if s not in VALID_STATUSES:
            print(f"Warning: '{s}' not in known statuses: {VALID_STATUSES}", file=sys.stderr)
        params["filter.overallStatus"] = s

    if phase:
        p = phase.upper()
        if p not in VALID_PHASES:
            print(f"Warning: '{p}' not in known phases: {VALID_PHASES}", file=sys.stderr)
        # API v2 uses filter.advanced for phase filtering
        params["filter.advanced"] = f"AREA[Phase]{p}"

    all_studies = []
    next_token = None
    total = None

    while len(all_studies) < limit:
        if next_token:
            params["pageToken"] = next_token
        params["pageSize"] = min(limit - len(all_studies), 100)

        resp = requests.get(API_BASE, params=params, timeout=30)
        resp.raise_for_status()
        data = resp.json()

        if total is None:
            total = data.get("totalCount", 0)

        studies = data.get("studies", [])
        if not studies:
            break
        all_studies.extend(studies)

        next_token = data.get("nextPageToken")
        if not next_token:
            break

    return all_studies, total or len(all_studies)


def parse_study(study):
    """Extract key fields from a study object."""
    proto = study.get("protocolSection", {})
    ident = proto.get("identificationModule", {})
    status_mod = proto.get("statusModule", {})
    design = proto.get("designModule", {})
    conds = proto.get("conditionsModule", {})
    arms = proto.get("armsInterventionsModule", {})
    sponsor_mod = proto.get("sponsorCollaboratorsModule", {})

    nct_id = ident.get("nctId", "")

    # Interventions
    interventions = []
    for iv in arms.get("interventions", []):
        interventions.append(iv.get("name", ""))

    # Sponsor
    lead = sponsor_mod.get("leadSponsor", {})
    sponsor = lead.get("name", "")

    # Dates
    start_info = status_mod.get("startDateStruct", {})
    comp_info = status_mod.get("completionDateStruct", {}) or status_mod.get("primaryCompletionDateStruct", {})
    start_date = start_info.get("date", "")
    completion_date = comp_info.get("date", "") if comp_info else ""

    # Enrollment
    enroll_info = design.get("enrollmentInfo", {})
    enrollment = enroll_info.get("count") if enroll_info else None

    # Phases - can be list like ["PHASE1", "PHASE2"] or single
    phases = design.get("phases", [])
    phase_str = "|".join(phases) if phases else "NA"

    return {
        "nct_id": nct_id,
        "title": ident.get("briefTitle", ""),
        "status": status_mod.get("overallStatus", ""),
        "phase": phase_str,
        "conditions": conds.get("conditions", []),
        "interventions": interventions,
        "sponsor": sponsor,
        "start_date": start_date,
        "completion_date": completion_date,
        "enrollment": enrollment,
        "study_type": design.get("studyType", ""),
        "url": f"https://clinicaltrials.gov/study/{nct_id}"
    }


def compute_stats(trials):
    """Compute aggregate statistics."""
    phase_ctr = Counter()
    status_ctr = Counter()
    sponsor_ctr = Counter()
    condition_ctr = Counter()

    for t in trials:
        for p in t["phase"].split("|"):
            phase_ctr[p] += 1
        status_ctr[t["status"]] += 1
        if t["sponsor"]:
            sponsor_ctr[t["sponsor"]] += 1
        for c in t["conditions"]:
            condition_ctr[c] += 1

    return {
        "by_phase": dict(phase_ctr.most_common()),
        "by_status": dict(status_ctr.most_common()),
        "top_sponsors": sponsor_ctr.most_common(15),
        "top_conditions": condition_ctr.most_common(15),
    }


def plot_bar(data_dict, title, xlabel, ylabel, filepath):
    """Generic horizontal bar chart."""
    if not data_dict:
        return
    labels = list(data_dict.keys())
    values = list(data_dict.values())

    fig, ax = plt.subplots(figsize=(10, max(4, len(labels) * 0.5)))
    ax.barh(range(len(labels)), values, color='steelblue')
    ax.set_yticks(range(len(labels)))
    ax.set_yticklabels(labels)
    ax.set_xlabel(ylabel)
    ax.set_title(title)
    ax.invert_yaxis()
    plt.tight_layout()
    plt.savefig(filepath, dpi=150, bbox_inches='tight')
    plt.close()


def plot_timeline(trials, title, filepath):
    """Timeline scatter of trial start dates."""
    dates = []
    labels = []
    for t in trials:
        d = t.get("start_date", "")
        if not d:
            continue
        try:
            # Handles "YYYY-MM-DD", "YYYY-MM", "Month YYYY", etc.
            for fmt in ("%Y-%m-%d", "%Y-%m", "%B %Y", "%B %d, %Y"):
                try:
                    dt = datetime.strptime(d, fmt)
                    dates.append(dt)
                    labels.append(t["nct_id"])
                    break
                except ValueError:
                    continue
        except Exception:
            continue

    if not dates:
        return

    fig, ax = plt.subplots(figsize=(12, max(4, len(dates) * 0.3)))
    y_pos = range(len(dates))
    ax.scatter(dates, y_pos, c='steelblue', s=40, zorder=3)
    ax.set_yticks(list(y_pos))
    ax.set_yticklabels(labels, fontsize=7)
    ax.set_xlabel("Start Date")
    ax.set_title(title)
    ax.grid(axis='x', alpha=0.3)
    ax.invert_yaxis()
    plt.tight_layout()
    plt.savefig(filepath, dpi=150, bbox_inches='tight')
    plt.close()


def main():
    parser = argparse.ArgumentParser(description="Query ClinicalTrials.gov for drug/condition trials.")
    parser.add_argument("--drug", help="Drug name or SMILES")
    parser.add_argument("--condition", help="Disease/condition")
    parser.add_argument("--status", help="Filter by status (e.g. RECRUITING, COMPLETED)")
    parser.add_argument("--phase", help="Filter by phase (e.g. PHASE3)")
    parser.add_argument("--limit", type=int, default=20, help="Max trials to fetch (default 20)")
    parser.add_argument("--output", default="./trials_output", help="Output directory")
    args = parser.parse_args()

    if not args.drug and not args.condition:
        parser.error("Must specify --drug or --condition (or both)")

    os.makedirs(args.output, exist_ok=True)

    drug_name = args.drug
    resolved_from_smiles = False

    # SMILES resolution
    if drug_name and is_smiles(drug_name):
        print(f"Resolving SMILES to drug name...", file=sys.stderr)
        resolved = resolve_smiles_to_name(drug_name)
        if resolved:
            print(f"Resolved to: {resolved}", file=sys.stderr)
            drug_name = resolved
            resolved_from_smiles = True
        else:
            print(f"Could not resolve SMILES, using as search term", file=sys.stderr)

    # Fetch
    print(f"Querying ClinicalTrials.gov: drug={drug_name}, condition={args.condition}, "
          f"status={args.status}, phase={args.phase}, limit={args.limit}", file=sys.stderr)

    try:
        studies, total_found = fetch_trials(
            drug=drug_name, condition=args.condition,
            status=args.status, phase=args.phase, limit=args.limit
        )
    except requests.RequestException as e:
        print(f"API error: {e}", file=sys.stderr)
        sys.exit(1)
    except ValueError as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)

    if not studies:
        print("No trials found.", file=sys.stderr)
        # Write empty summary
        label = drug_name or args.condition or "query"
        summary = {
            "drug": drug_name, "query": vars(args),
            "total_found": 0, "trials": [], "stats": {}
        }
        with open(os.path.join(args.output, f"{label}_trials_summary.json"), 'w') as f:
            json.dump(summary, f, indent=2)
        sys.exit(0)

    # Parse
    trials = [parse_study(s) for s in studies]
    stats = compute_stats(trials)

    # File label
    label = (drug_name or args.condition or "query").replace(" ", "_").replace("/", "_")

    summary = {
        "drug": drug_name,
        "query": {
            "drug": drug_name,
            "condition": args.condition,
            "status": args.status,
            "phase": args.phase,
            "resolved_from_smiles": resolved_from_smiles,
        },
        "total_found": total_found,
        "trials_returned": len(trials),
        "trials": trials,
        "stats": stats,
    }

    summary_path = os.path.join(args.output, f"{label}_trials_summary.json")
    with open(summary_path, 'w') as f:
        json.dump(summary, f, indent=2)
    print(f"Summary: {summary_path}", file=sys.stderr)

    # Plots
    try:
        plot_bar(stats["by_phase"],
                 f"Clinical Trials by Phase: {drug_name or args.condition}",
                 "Phase", "Count",
                 os.path.join(args.output, f"{label}_trials_by_phase.png"))
    except Exception as e:
        print(f"Plot error (phase): {e}", file=sys.stderr)

    try:
        plot_bar(stats["by_status"],
                 f"Clinical Trials by Status: {drug_name or args.condition}",
                 "Status", "Count",
                 os.path.join(args.output, f"{label}_trials_by_status.png"))
    except Exception as e:
        print(f"Plot error (status): {e}", file=sys.stderr)

    try:
        plot_timeline(trials,
                      f"Trial Start Date Timeline: {drug_name or args.condition}",
                      os.path.join(args.output, f"{label}_trials_timeline.png"))
    except Exception as e:
        print(f"Plot error (timeline): {e}", file=sys.stderr)

    # Print summary to stdout
    print(f"\n=== ClinicalTrials.gov Results ===")
    print(f"Drug/Query: {drug_name or args.condition}")
    print(f"Total found: {total_found}")
    print(f"Returned: {len(trials)}")
    print(f"By phase: {stats['by_phase']}")
    print(f"By status: {stats['by_status']}")
    print(f"Top sponsors: {stats['top_sponsors'][:5]}")
    print(f"Files saved to: {args.output}/")


if __name__ == "__main__":
    main()


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