Source code for tooluniverse.sabdab_tool

"""
SAbDab (Structural Antibody Database) tool for ToolUniverse.

SAbDab is a database containing all antibody structures from the PDB,
annotated with CDR sequences, chain pairings, and other structural features.

Website: https://opig.stats.ox.ac.uk/webapps/sabdab-sabpred/sabdab
"""

import requests
from typing import Dict, Any, Optional, List
from .base_tool import BaseTool
from .tool_registry import register_tool

# SAbDab base URL
SABDAB_BASE_URL = "https://opig.stats.ox.ac.uk/webapps/sabdab-sabpred/sabdab"


[docs] @register_tool("SAbDabTool") class SAbDabTool(BaseTool): """ Tool for querying SAbDab structural antibody database. SAbDab provides: - Antibody structures from PDB - CDR (complementarity-determining region) annotations - Heavy/light chain pairing information - Antigen binding information No authentication required. """
[docs] def __init__(self, tool_config: Dict[str, Any]): super().__init__(tool_config) self.timeout: int = tool_config.get("timeout", 60) self.parameter = tool_config.get("parameter", {})
[docs] def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Execute SAbDab query based on operation type.""" operation = arguments.get("operation", "") if operation == "search_structures": return self._search_structures(arguments) elif operation == "get_structure": return self._get_structure(arguments) elif operation == "get_summary": return self._get_summary(arguments) else: return { "status": "error", "error": f"Unknown operation: {operation}. Supported: search_structures, get_structure, get_summary", }
[docs] def _search_structures(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """ Search SAbDab for antibody structures. Args: arguments: Dict containing: - query: Search query (antigen name, species, etc.) - limit: Maximum results """ query = arguments.get("query", "") limit = arguments.get("limit", 50) try: # SAbDab search endpoint response = requests.get( f"{SABDAB_BASE_URL}/search/", params={"q": query, "limit": limit}, timeout=self.timeout, headers={ "User-Agent": "ToolUniverse/SAbDab", "Accept": "application/json", }, ) # SAbDab may return HTML, handle gracefully if "json" in response.headers.get("Content-Type", ""): data = response.json() structures = data if isinstance(data, list) else data.get("results", []) else: # Provide guidance for web-only access return { "status": "success", "data": { "message": "SAbDab search requires web interface", "search_url": f"{SABDAB_BASE_URL}/search/?q={query}", "suggestion": "Visit the URL above to search SAbDab", }, "metadata": { "source": "SAbDab", "note": "Web interface required for full search", }, } return { "status": "success", "data": { "structures": structures, "count": len(structures), "query": query, }, "metadata": { "source": "SAbDab", }, } except requests.exceptions.RequestException as e: return {"status": "error", "error": f"Request failed: {str(e)}"} except Exception as e: return {"status": "error", "error": f"Unexpected error: {str(e)}"}
[docs] def _get_structure(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """ Get antibody structure details by PDB ID. Args: arguments: Dict containing: - pdb_id: 4-character PDB ID """ pdb_id = arguments.get("pdb_id", "") if not pdb_id: return {"status": "error", "error": "Missing required parameter: pdb_id"} # SAbDab API requires lowercase PDB IDs pdb_id_lower = pdb_id.lower() try: # Use direct PDB download endpoint (Chothia numbering) pdb_url = f"{SABDAB_BASE_URL}/pdb/{pdb_id_lower}/" response = requests.get( pdb_url, timeout=self.timeout, headers={"User-Agent": "ToolUniverse/SAbDab"}, ) if response.status_code == 404: return { "status": "error", "error": f"Structure not found: {pdb_id}. Note: SAbDab may not have all PDB structures.", } response.raise_for_status() # Extract metadata from PDB REMARK lines pdb_content = response.text metadata = {"pdb_id": pdb_id} # Parse REMARK 5 lines which contain SAbDab annotations for line in pdb_content.split("\n"): if line.startswith("REMARK 5 PAIRED_"): # Extract chain pairing info parts = line.split() for i, part in enumerate(parts): if "=" in part: key, val = part.split("=") metadata[key.lower()] = val elif line.startswith("REMARK 5 "): # Store other remarks remark = line[15:].strip() if remark and "remarks" not in metadata: metadata["remarks"] = [] if remark and remark not in str(metadata.get("remarks", [])): if "remarks" in metadata: metadata["remarks"].append(remark) return { "status": "success", "data": { "pdb_id": pdb_id, "download_url": pdb_url, "structure_url": f"{SABDAB_BASE_URL}/structureviewer/?pdb={pdb_id}", "search_url": f"{SABDAB_BASE_URL}/search/?pdb={pdb_id}", "metadata": metadata, "pdb_size_bytes": len(pdb_content), "pdb_preview": pdb_content[:500] if len(pdb_content) > 500 else pdb_content, }, "metadata": { "source": "SAbDab", "note": "PDB file with Chothia numbering available at download_url", }, } except requests.exceptions.RequestException as e: return {"status": "error", "error": f"Request failed: {str(e)}"} except Exception as e: return {"status": "error", "error": f"Unexpected error: {str(e)}"}
[docs] def _get_summary(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """ Get SAbDab database summary statistics. Args: arguments: Dict (no required parameters) """ try: response = requests.get( f"{SABDAB_BASE_URL}/stats/", timeout=self.timeout, headers={ "User-Agent": "ToolUniverse/SAbDab", "Accept": "application/json", }, ) if "json" in response.headers.get("Content-Type", ""): data = response.json() else: # Return static info about SAbDab data = { "description": "SAbDab - Structural Antibody Database", "content": "All antibody structures from PDB with annotations", "features": [ "CDR sequence annotations", "Heavy/light chain pairing", "Antigen information", "Species classification", ], "url": SABDAB_BASE_URL, } return { "status": "success", "data": data, "metadata": { "source": "SAbDab", }, } except Exception as e: return {"status": "error", "error": f"Unexpected error: {str(e)}"}