| """ |
| Protocols.io API integration for searching and retrieving biological protocols. |
| |
| This module provides functions to search protocols.io for relevant protocols |
| based on natural language queries. |
| """ |
|
|
| import os |
| from typing import Any |
|
|
| import requests |
|
|
| try: |
| |
| from biomni.config import default_config |
| except Exception: |
| default_config = None |
|
|
|
|
| |
| PROTOCOLS_IO_API_BASE = "https://www.protocols.io/api/v3" |
|
|
| |
| ACCESS_TOKEN = os.getenv("PROTOCOLS_IO_ACCESS_TOKEN") or os.getenv("BIOMNI_PROTOCOLS_IO_ACCESS_TOKEN") |
| if not ACCESS_TOKEN and default_config is not None: |
| ACCESS_TOKEN = getattr(default_config, "protocols_io_access_token", None) |
|
|
|
|
| def search_protocols( |
| query: str, |
| ) -> dict[str, Any]: |
| """ |
| Search protocols.io for relevant protocols based on a natural language query. |
| |
| Args: |
| query: Keyword search term to find protocols (searches names, descriptions, authors), should be EXACT as we are looking for EXACT MATCH |
| |
| Returns: |
| Dictionary containing: |
| - protocols: List of protocol dictionaries with title, description, url, etc. |
| - pagination: Pagination information |
| - total_results: Total number of matching protocols |
| - status_code: API status code (0 = success) |
| |
| Raises: |
| requests.RequestException: If API request fails |
| ValueError: If invalid parameters are provided |
| |
| Example: |
| >>> results = search_protocols("CRISPR gene editing", page_size=5) |
| >>> for protocol in results["protocols"]: |
| ... print(f"{protocol['title']}: {protocol['url']}") |
| """ |
| |
| if not query or not query.strip(): |
| raise ValueError("Query cannot be empty") |
| |
| if not ACCESS_TOKEN: |
| raise ValueError( |
| "Protocols.io access token is not configured. Set PROTOCOLS_IO_ACCESS_TOKEN or BIOMNI_PROTOCOLS_IO_ACCESS_TOKEN env var, or configure BiomniConfig.protocols_io_access_token." |
| ) |
|
|
| |
| url = "https://www.protocols.io/api/v3/protocols" |
|
|
| headers = {"Authorization": f"Bearer {ACCESS_TOKEN}", "Content-Type": "application/json"} |
|
|
| |
| search_key = query.strip() |
|
|
| params = { |
| "filter": "public", |
| "key": search_key, |
| } |
|
|
| try: |
| |
| response = requests.get(url, headers=headers, params=params) |
| response.raise_for_status() |
|
|
| data = response.json() |
|
|
| |
| if data.get("status_code") != 0: |
| return { |
| "protocols": [], |
| "pagination": {}, |
| "total_results": 0, |
| "status_code": data.get("status_code"), |
| "error": data.get("error_message", "Unknown error"), |
| } |
|
|
| |
| protocols = [] |
| for item in data.get("items", []): |
| protocol_info = { |
| "id": item.get("id"), |
| "title": item.get("title", ""), |
| "description": item.get("description", ""), |
| "url": item.get("uri", ""), |
| "doi": item.get("doi", ""), |
| "authors": [ |
| {"name": author.get("name", ""), "username": author.get("username", "")} |
| for author in item.get("authors", []) |
| ], |
| "created_on": item.get("created_on"), |
| "published_on": item.get("published_on"), |
| "version": item.get("version_id"), |
| "views": item.get("nr_of_views", 0), |
| "is_peer_reviewed": item.get("is_peer_reviewed", False), |
| "tags": item.get("tags", []), |
| } |
| protocols.append(protocol_info) |
|
|
| |
| pagination = data.get("pagination", {}) |
|
|
| return { |
| "protocols": protocols, |
| "pagination": { |
| "current_page": pagination.get("current_page", 1), |
| "total_pages": pagination.get("total_pages", 1), |
| "page_size": pagination.get("page_size", 1), |
| "total_results": pagination.get("total_results", 0), |
| }, |
| "total_results": pagination.get("total_results", 0), |
| "status_code": 0, |
| } |
|
|
| except requests.exceptions.RequestException as e: |
| raise requests.RequestException(f"Failed to search protocols.io: {str(e)}") from e |
|
|
|
|
| def get_protocol_details(protocol_id: int, timeout: int = 30) -> dict[str, Any]: |
| """ |
| Get detailed information about a specific protocol by ID. |
| |
| Args: |
| protocol_id: The protocol ID from protocols.io |
| timeout: Request timeout in seconds (default: 30) |
| |
| Returns: |
| Dictionary containing detailed protocol information |
| |
| Raises: |
| requests.RequestException: If API request fails |
| """ |
| |
| if not ACCESS_TOKEN: |
| raise ValueError( |
| "Protocols.io access token is not configured. Set PROTOCOLS_IO_ACCESS_TOKEN or BIOMNI_PROTOCOLS_IO_ACCESS_TOKEN env var, or configure BiomniConfig.protocols_io_access_token." |
| ) |
|
|
| url = f"https://www.protocols.io/api/v3/protocols/{protocol_id}" |
|
|
| headers = {"Authorization": f"Bearer {ACCESS_TOKEN}", "Content-Type": "application/json"} |
|
|
| try: |
| response = requests.get(url, headers=headers, timeout=timeout) |
| response.raise_for_status() |
|
|
| data = response.json() |
|
|
| if data.get("status_code") != 0: |
| return {"error": data.get("error_message", "Unknown error"), "status_code": data.get("status_code")} |
|
|
| return data.get("protocol", {}) |
|
|
| except requests.exceptions.RequestException as e: |
| raise requests.RequestException(f"Failed to get protocol details: {str(e)}") from e |
|
|
|
|
| def _get_protocols_directory(): |
| """Get the path to the local protocols directory.""" |
| import biomni |
|
|
| return os.path.join(os.path.dirname(biomni.__file__), "tool", "protocols") |
|
|
|
|
| def list_local_protocols(source: str | None = None) -> dict[str, Any]: |
| """ |
| List available protocol files in the local biomni/tool/protocols/ directory. |
| |
| Args: |
| source: Optional filter by source directory (e.g., 'addgene' or 'thermofisher'). |
| If None, lists all protocols from all sources. |
| |
| Returns: |
| Dictionary containing: |
| - protocols: List of protocol file information with name, source, and path |
| - total_count: Total number of protocols found |
| - sources: List of available source directories |
| |
| Example: |
| >>> results = list_local_protocols() |
| >>> results = list_local_protocols(source="addgene") |
| """ |
| protocols_dir = _get_protocols_directory() |
|
|
| if not os.path.exists(protocols_dir): |
| return { |
| "protocols": [], |
| "total_count": 0, |
| "sources": [], |
| "error": f"Protocols directory not found: {protocols_dir}", |
| } |
|
|
| protocols = [] |
| sources = [] |
|
|
| |
| for item in os.listdir(protocols_dir): |
| item_path = os.path.join(protocols_dir, item) |
| if os.path.isdir(item_path): |
| sources.append(item) |
|
|
| |
| search_dirs = [source] if source and source in sources else sources |
|
|
| |
| for source_dir in search_dirs: |
| source_path = os.path.join(protocols_dir, source_dir) |
| if os.path.isdir(source_path): |
| for filename in os.listdir(source_path): |
| if filename.endswith(".txt"): |
| file_path = os.path.join(source_path, filename) |
| protocols.append( |
| { |
| "name": filename, |
| "source": source_dir, |
| "path": file_path, |
| "relative_path": os.path.join(source_dir, filename), |
| } |
| ) |
|
|
| return { |
| "protocols": sorted(protocols, key=lambda x: (x["source"], x["name"])), |
| "total_count": len(protocols), |
| "sources": sorted(sources), |
| } |
|
|
|
|
| def read_local_protocol(filename: str, source: str | None = None) -> dict[str, Any]: |
| """ |
| Read the contents of a local protocol file from biomni/tool/protocols/. |
| |
| Args: |
| filename: Name of the protocol file (e.g., 'Addgene_ Protocol - How to Run an Agarose Gel.txt') |
| source: Optional source directory (e.g., 'addgene' or 'thermofisher'). |
| If None, searches all source directories. |
| |
| Returns: |
| Dictionary containing: |
| - content: Full text content of the protocol file |
| - filename: Name of the file |
| - source: Source directory where the file was found |
| - path: Full path to the file |
| |
| Raises: |
| FileNotFoundError: If the protocol file is not found |
| ValueError: If filename is empty |
| |
| Example: |
| >>> protocol = read_local_protocol("Addgene_ Protocol - How to Run an Agarose Gel.txt", source="addgene") |
| >>> print(protocol["content"]) |
| """ |
| if not filename or not filename.strip(): |
| raise ValueError("Filename cannot be empty") |
|
|
| filename = filename.strip() |
| protocols_dir = _get_protocols_directory() |
|
|
| if not os.path.exists(protocols_dir): |
| raise FileNotFoundError(f"Protocols directory not found: {protocols_dir}") |
|
|
| |
| if source: |
| file_path = os.path.join(protocols_dir, source, filename) |
| if os.path.exists(file_path): |
| with open(file_path, encoding="utf-8") as f: |
| content = f.read() |
| return { |
| "content": content, |
| "filename": filename, |
| "source": source, |
| "path": file_path, |
| } |
| raise FileNotFoundError(f"Protocol file not found: {source}/{filename}") |
|
|
| |
| for source_dir in os.listdir(protocols_dir): |
| source_path = os.path.join(protocols_dir, source_dir) |
| if os.path.isdir(source_path): |
| file_path = os.path.join(source_path, filename) |
| if os.path.exists(file_path): |
| with open(file_path, encoding="utf-8") as f: |
| content = f.read() |
| return { |
| "content": content, |
| "filename": filename, |
| "source": source_dir, |
| "path": file_path, |
| } |
|
|
| raise FileNotFoundError(f"Protocol file not found in any source directory: {filename}") |
|
|