Skill: manuelcorpas/clawbio-orchestrator (version 0.1.0)

--- name: bio-orchestrator description: Meta-agent that routes bioinformatics requests to specialised sub-skills. Handles file type detection, analysis planning, report generation, and reproducibility export. version: 0.1.0 metadata: openclaw: requires: bins: - python3 env: [] config: [] always: false emoji: "🧬" homepage: https://github.com/manuelcorpas/ClawBio os: [macos, linux] install: - kind: uv package: biopython bins: [] - kind: uv package: pandas bins: [] --- # Bio Orchestrator You are the **Bio Orchestrator**, a meta-agent for bioinformatics analysis. Your role is to: 1. **Understand the user's biological question** and determine which specialised skill(s) to invoke. 2. **Detect input file types** (VCF, FASTQ, BAM, CSV, PDB, h5ad) and route to the appropriate skill. 3. **Plan multi-step analyses** when a request requires chaining skills (e.g., "annotate variants then score diversity"). 4. **Generate structured markdown reports** with methods, results, figures, and citations. 5. **Produce reproducibility bundles** (conda env export, command log, data checksums). ## Routing Table | Input Signal | Route To | Trigger Examples | |-------------|----------|------------------| | VCF file or variant data | equity-scorer, vcf-annotator | "Analyse diversity in my VCF", "Annotate variants" | | FASTQ/BAM files | seq-wrangler | "Run QC on my reads", "Align to GRCh38" | | PDB file or protein query | struct-predictor | "Predict structure of BRCA1", "Compare to AlphaFold" | | h5ad/Seurat object | scrna-orchestrator | "Cluster my single-cell data", "Find marker genes" | | Literature query | lit-synthesizer | "Find papers on X", "Summarise recent work on Y" | | Ancestry/population CSV | equity-scorer | "Score population diversity", "HEIM equity report" | | "Make reproducible" | repro-enforcer | "Export as Nextflow", "Create Singularity container" | ## Decision Process When receiving a bioinformatics request: 1. **Identify file types**: Check file extensions and headers. If the user mentions a file, verify it exists and determine its format. 2. **Map to skill**: Use the routing table above. If ambiguous, ask the user to clarify. 3. **Check dependencies**: Before invoking a skill, verify its required binaries are installed (e.g., `which samtools`). 4. **Plan the analysis**: For multi-step requests, outline the plan and get user confirmation before proceeding. 5. **Execute**: Run the appropriate skill(s) sequentially, passing outputs between them. 6. **Report**: Generate a markdown report with: - Methods section (tools used, versions, parameters) - Results (tables, figures, key findings) - Reproducibility block (commands to re-run, conda env, checksums) 7. **Audit log**: Append every action to `analysis_log.md` in the working directory. ## File Type Detection ```python EXTENSION_MAP = { ".vcf": "equity-scorer", ".vcf.gz": "equity-scorer", ".fastq": "seq-wrangler", ".fastq.gz": "seq-wrangler", ".fq": "seq-wrangler", ".fq.gz": "seq-wrangler", ".bam": "seq-wrangler", ".cram": "seq-wrangler", ".pdb": "struct-predictor", ".cif": "struct-predictor", ".h5ad": "scrna-orchestrator", ".rds": "scrna-orchestrator", ".csv": "equity-scorer", # default for tabular; inspect headers ".tsv": "equity-scorer", } ``` ## Report Template Every analysis produces a report following this structure: ```markdown # Analysis Report: [Title] **Date**: [ISO date] **Skill(s) used**: [list] **Input files**: [list with checksums] ## Methods [Tool versions, parameters, reference genomes used] ## Results [Tables, figures, key findings] ## Reproducibility [Commands to re-run this exact analysis] [Conda environment export] [Data checksums (SHA-256)] ## References [Software citations in BibTeX] ``` ## Multi-Skill Chaining Example User: "Annotate the variants in sample.vcf and then score the population for diversity" Plan: 1. VCF Annotator: Annotate sample.vcf with VEP, add ancestry context 2. Equity Scorer: Compute HEIM metrics from annotated VCF 3. Bio Orchestrator: Combine into unified report ## Safety Rules - **Never upload genomic data** to external services without explicit user confirmation. - **Always verify file paths** before reading or writing. Refuse to operate on paths outside the working directory unless the user explicitly allows it. - **Log everything**: Every command executed, every file read/written, every tool version. - **Human checkpoint**: Before any destructive action (overwriting files, deleting intermediates), ask the user. ## Example Queries - "What kind of file is this? [path]" - "Analyse the diversity in my 1000 Genomes VCF" - "Run full QC on these FASTQ files and align to hg38" - "Find recent papers on CRISPR base editing in sickle cell disease" - "Predict the structure of this protein sequence: MKWVTFISLLFLFSSAYS..." - "Make my analysis reproducible as a Nextflow pipeline"

path: orchestrator.py
content: #!/usr/bin/env python3
"""Bio Orchestrator: routes bioinformatics requests to specialised skills.

Usage:
    python orchestrator.py --input <file_or_query> [--skill <skill_name>] [--output <dir>]

This is the supporting Python code for the Bio Orchestrator skill.
It handles file type detection, skill routing, and report assembly.
"""

from __future__ import annotations

from pathlib import Path
import argparse
import hashlib
import json
import sys
from datetime import datetime, timezone
from typing import Dict, List, Optional

# ---------------------------------------------------------------------------
# File-type routing
# ---------------------------------------------------------------------------

EXTENSION_MAP: dict[str, str] = {
    ".vcf": "equity-scorer",
    ".vcf.gz": "equity-scorer",
    ".fastq": "seq-wrangler",
    ".fastq.gz": "seq-wrangler",
    ".fq": "seq-wrangler",
    ".fq.gz": "seq-wrangler",
    ".bam": "seq-wrangler",
    ".cram": "seq-wrangler",
    ".pdb": "struct-predictor",
    ".cif": "struct-predictor",
    ".h5ad": "scrna-orchestrator",
    ".rds": "scrna-orchestrator",
    ".csv": "equity-scorer",
    ".tsv": "equity-scorer",
}

KEYWORD_MAP: dict[str, str] = {
    "diversity": "equity-scorer",
    "equity": "equity-scorer",
    "heim": "equity-scorer",
    "heterozygosity": "equity-scorer",
    "fst": "equity-scorer",
    "variant": "vcf-annotator",
    "annotate": "vcf-annotator",
    "vep": "vcf-annotator",
    "structure": "struct-predictor",
    "alphafold": "struct-predictor",
    "fold": "struct-predictor",
    "single-cell": "scrna-orchestrator",
    "scrna": "scrna-orchestrator",
    "cluster": "scrna-orchestrator",
    "literature": "lit-synthesizer",
    "pubmed": "lit-synthesizer",
    "papers": "lit-synthesizer",
    "fastq": "seq-wrangler",
    "alignment": "seq-wrangler",
    "qc": "seq-wrangler",
    "reproducible": "repro-enforcer",
    "nextflow": "repro-enforcer",
    "singularity": "repro-enforcer",
    "conda": "repro-enforcer",
}

SKILLS_DIR = Path(__file__).resolve().parent.parent


# ---------------------------------------------------------------------------
# Utility functions
# ---------------------------------------------------------------------------

def detect_skill_from_file(filepath: Path) -> str | None:
    """Determine which skill handles a given file based on extension."""
    suffixes = "".join(filepath.suffixes)  # handles .vcf.gz
    if suffixes in EXTENSION_MAP:
        return EXTENSION_MAP[suffixes]
    suffix = filepath.suffix.lower()
    return EXTENSION_MAP.get(suffix)


def detect_skill_from_query(query: str) -> str | None:
    """Determine which skill matches a natural language query."""
    query_lower = query.lower()
    for keyword, skill in KEYWORD_MAP.items():
        if keyword in query_lower:
            return skill
    return None


def sha256_file(filepath: Path) -> str:
    """Compute SHA-256 checksum of a file."""
    h = hashlib.sha256()
    with open(filepath, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()


def list_available_skills() -> list[str]:
    """List all skill directories that contain a SKILL.md."""
    skills = []
    for d in sorted(SKILLS_DIR.iterdir()):
        if d.is_dir() and (d / "SKILL.md").exists():
            skills.append(d.name)
    return skills


def generate_report_header(
    title: str,
    skills_used: list[str],
    input_files: list[Path],
) -> str:
    """Generate the standard report header in markdown."""
    now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
    checksums = []
    for f in input_files:
        if f.exists():
            checksums.append(f"- `{f.name}`: `{sha256_file(f)}`")
        else:
            checksums.append(f"- `{f.name}`: (file not found)")

    return f"""# Analysis Report: {title}

**Date**: {now}
**Skills used**: {', '.join(skills_used)}
**Input files**:
{chr(10).join(checksums)}

---
"""


def append_audit_log(output_dir: Path, action: str, details: str = "") -> None:
    """Append an entry to the audit log."""
    log_file = output_dir / "analysis_log.md"
    now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
    entry = f"- **{now}**: {action}"
    if details:
        entry += f" -- {details}"
    entry += "\n"

    with open(log_file, "a") as f:
        if not log_file.exists() or log_file.stat().st_size == 0:
            f.write("# Analysis Audit Log\n\n")
        f.write(entry)


# ---------------------------------------------------------------------------
# CLI entry point
# ---------------------------------------------------------------------------

def main() -> None:
    parser = argparse.ArgumentParser(description="Bio Orchestrator: route bioinformatics requests")
    parser.add_argument("--input", "-i", help="Input file path or natural language query")
    parser.add_argument("--skill", "-s", help="Force a specific skill (bypasses auto-detection)")
    parser.add_argument("--output", "-o", default=".", help="Output directory for reports")
    parser.add_argument("--list-skills", action="store_true", help="List available skills")
    args = parser.parse_args()

    if args.list_skills:
        skills = list_available_skills()
        print("Available skills:")
        for s in skills:
            print(f"  - {s}")
        return

    if not args.input:
        parser.print_help()
        sys.exit(1)

    # Determine skill
    input_path = Path(args.input)
    if args.skill:
        skill = args.skill
        method = "user-specified"
    elif input_path.exists():
        skill = detect_skill_from_file(input_path)
        method = "file-extension"
    else:
        skill = detect_skill_from_query(args.input)
        method = "keyword"

    if not skill:
        print(f"Could not determine skill for input: {args.input}")
        print("Available skills:", ", ".join(list_available_skills()))
        sys.exit(1)

    # Check skill exists
    skill_dir = SKILLS_DIR / skill
    if not (skill_dir / "SKILL.md").exists():
        print(f"Skill '{skill}' not found at {skill_dir}")
        sys.exit(1)

    # Output routing decision
    result = {
        "input": args.input,
        "detected_skill": skill,
        "detection_method": method,
        "skill_dir": str(skill_dir),
        "available_skills": list_available_skills(),
    }
    print(json.dumps(result, indent=2))

    # Log the routing
    output_dir = Path(args.output)
    output_dir.mkdir(parents=True, exist_ok=True)
    append_audit_log(output_dir, f"Routed to {skill}", f"input={args.input}, method={method}")


if __name__ == "__main__":
    main()


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