From 1d8b2835256070ddb593ba25da096925898ae6e6 Mon Sep 17 00:00:00 2001 From: Hernan Date: Fri, 24 Jul 2026 11:32:49 -0300 Subject: [PATCH 1/4] Migrate Modulector integration to SDK Replaced direct Modulector HTTP calls with modulector-sdk wrappers in `MRNAService` and updated all related consumers (`api_service`, `biomarkers`, `molecules_details`, and assistant tools). Added dedicated SDK-backed methods for miRNA/methylation lookup, interactions, diseases, drugs, and code/site search, plus paginated response normalization and safer fallbacks. Also introduced `MODULECTOR_BASE_URL` settings, added `modulector-sdk` dependency, updated MCP server command args for BioMCP, and fixed a null-response edge case in biomarker gene lookup. --- config/mcp_servers.json | 4 +- config/requirements.txt | 3 +- src/api_service/mongo_service.py | 7 +- src/api_service/mrna_service.py | 209 ++++++++++++++++++------ src/api_service/views.py | 52 +++--- src/assistant/services/tools.py | 133 ++++++++++++++- src/biomarkers/views.py | 27 +-- src/molecules_details/views.py | 9 +- src/multiomics_intermediate/settings.py | 14 +- 9 files changed, 326 insertions(+), 132 deletions(-) diff --git a/config/mcp_servers.json b/config/mcp_servers.json index e79f2593..53313b39 100644 --- a/config/mcp_servers.json +++ b/config/mcp_servers.json @@ -5,8 +5,8 @@ "enabled": true, "description": "Biomedical research: PubMed papers, bioRxiv, ClinicalTrials.gov, NCI, variants, OncoKB", "transport": "stdio", - "command": "biomcp", - "args": ["run", "--mode", "stdio"], + "command": "biomcp-cli", + "args": ["mcp"], "env": {} } } diff --git a/config/requirements.txt b/config/requirements.txt index 0cc208d0..1a3603de 100644 --- a/config/requirements.txt +++ b/config/requirements.txt @@ -38,4 +38,5 @@ langchain-mcp-adapters==0.2.2 pgvector==0.4.2 sentence-transformers==3.0.0 openai==2.38.0 -biomcp-cli==0.8.22 \ No newline at end of file +biomcp-cli==0.8.22 +modulector-sdk==2.4.0 \ No newline at end of file diff --git a/src/api_service/mongo_service.py b/src/api_service/mongo_service.py index 02cc2b13..cb8ac047 100644 --- a/src/api_service/mongo_service.py +++ b/src/api_service/mongo_service.py @@ -222,12 +222,7 @@ def __get_standard_ids(file_type: FileType, molecules: List[str]) -> Optional[Di method='post' ) elif file_type == FileType.MIRNA: - data = global_mrna_service.get_modulector_service_content( - 'mirna-codes', - request_params={'mirna_codes': molecules}, - is_paginated=False, - method='post' - ) + data = global_mrna_service.get_mirna_codes(mirna_codes=molecules) else: # In case of methylation, cBioPortal don't manage the methylation sites, so we don't need to use Modulector. # Generates a dummy dict with the same keys and values as the molecules list diff --git a/src/api_service/mrna_service.py b/src/api_service/mrna_service.py index d431f53e..04ec4910 100644 --- a/src/api_service/mrna_service.py +++ b/src/api_service/mrna_service.py @@ -1,35 +1,36 @@ import logging from json.decoder import JSONDecodeError -from typing import Any, Dict, Optional, Literal, Union +from typing import Any, Dict, List, Optional, Literal, Union import requests from django.conf import settings from django.http import QueryDict from requests.exceptions import ConnectionError +import modulector_sdk as modulector +from modulector_sdk import PaginatedResponse + class MRNAService(object): - url_modulector_prefix: str url_bioapi_prefix: str + _modulector_base_url: str def __init__(self): - modulector_settings = settings.MODULECTOR_SETTINGS - self.url_modulector_prefix = self.__build_url(modulector_settings) - bioapi_settings = settings.BIOAPI_SETTINGS self.url_bioapi_prefix = self.__build_url(bioapi_settings) + self._modulector_base_url = settings.MODULECTOR_BASE_URL @staticmethod - def __build_url(settings: Dict[str, Any]) -> str: + def __build_url(svc_settings: Dict[str, Any]) -> str: """ Constructs the URL based on the settings provided. If the port is the default for the protocol (80 for http, 443 for https), it is omitted. Otherwise, the port is included in the URL. - @param settings: Dictionary containing protocol, host, and port information. + @param svc_settings: Dictionary containing protocol, host, and port information. @return: Constructed URL as a string. """ - protocol = settings['protocol'] - host = settings['host'] - port = settings['port'] + protocol = svc_settings['protocol'] + host = svc_settings['host'] + port = svc_settings['port'] if (protocol == 'http' and port == 80) or (protocol == 'https' and port == 443): return f"{protocol}://{host}" @@ -41,43 +42,35 @@ def __generate_rest_query_params(get_request: QueryDict) -> str: """ Generates a string with all the query params from GET request. @param get_request: GET request with query params to send to DRF backend - @return: String to send to Modulector/BioAPI APIs + @return: String to send to BioAPI """ return '&'.join([f'{key}={value}' for (key, value) in get_request.items()]) - def __get_service_content( + def __get_bioapi_content( self, service_name: str, request_params: QueryDict, is_paginated: bool, - url_prefix: str, method: Literal['get', 'post'], - append_slash: bool ) -> Optional[Union[Dict, str]]: """ - Generic function to make a request to a Modulector/BioAPI service - @param service_name: Modulector/BioAPI service to consume - @param request_params: GET/POST request with query params to send to DRF backend - @param is_paginated: True if the expected response is paginated to generate a default response in case of error - @param url_prefix: URL of the Modulector or BioAPI service + Generic function to make a request to a BioAPI service. + @param service_name: BioAPI service to consume + @param request_params: GET/POST request with query params + @param is_paginated: True if the expected response is paginated @param method: Request method (GET or POST) - @param append_slash: If True appends a slash to prevent issues with Django. - @return: JSON data retrieved from the Modulector service. None if response has 404 status code + @return: JSON data retrieved from BioAPI. None if response has 404 status code """ - url = f'{url_prefix}/{service_name}' + url = f'{self.url_bioapi_prefix}/{service_name}/' data = None # Prevents Mypy warning try: if method == 'get': params = self.__generate_rest_query_params(request_params) if params: - url += f'/?{params}' + url += f'/?{params}/' data = requests.get(url) else: - # Prevents issues with Django APPEND_SLASH option - if append_slash and not url.endswith('/'): - url += '/' - data = requests.post(url, json=request_params) if data.status_code != 200: @@ -94,7 +87,7 @@ def __get_service_content( return None except (ConnectionError, JSONDecodeError) as ex: - logging.error(f'Received data from Modulector/BioAPI: {data}') + logging.error(f'Received data from BioAPI: {data}') logging.exception(ex) if is_paginated: @@ -106,24 +99,6 @@ def __get_service_content( } return None - def get_modulector_service_content( - self, - service_name: str, - request_params: QueryDict, - is_paginated: bool, - method: Literal['get', 'post'] = 'get' - ) -> Optional[Dict]: - """ - Makes a request to a Modulector service. - @param service_name: Modulector service to consume - @param request_params: GET/POST params with query params to send to DRF backend - @param is_paginated: True if the expected response is paginated to generate a default response in case of error - @param method: Request method (GET or POST) - @return: JSON data retrieved from the Modulector service. None if response has 404 status code - """ - return self.__get_service_content(service_name, request_params, is_paginated, self.url_modulector_prefix, - method, append_slash=True) - def get_bioapi_service_content( self, service_name: str, @@ -135,12 +110,144 @@ def get_bioapi_service_content( Makes a request to a BioAPI service. @param service_name: BioAPI service to consume @param request_params: GET/POST params with query params to send to DRF backend - @param is_paginated: True if the expected response is paginated to generate a default response in case of error + @param is_paginated: True if the expected response is paginated @param method: Request method (GET or POST) - @return: JSON data retrieved from the Modulector service. None if response has 404 status code + @return: JSON data retrieved from BioAPI. None if response has 404 status code """ - return self.__get_service_content(service_name, request_params, is_paginated, self.url_bioapi_prefix, method, - append_slash=False) + return self.__get_bioapi_content(service_name, request_params, is_paginated, method) + + # ------------------------------------------------------------------ # + # Modulector SDK wrappers # + # ------------------------------------------------------------------ # + + def _paginated_to_dict(self, result: Optional[PaginatedResponse]) -> Dict: + """Converts a SDK PaginatedResponse to the dict format expected by the frontend.""" + if result is None: + return {'count': 0, 'next': '', 'previous': '', 'results': []} + return { + 'count': result.count, + 'next': result.next or '', + 'previous': result.previous or '', + 'results': list(result.results or []), + } + + def get_mirna_details(self, mirna: str) -> Optional[Dict]: + """Get miRNA details from Modulector SDK.""" + try: + return modulector.get_mirna_details(mirna=mirna, base_url=self._modulector_base_url) + except Exception as ex: + logging.exception(ex) + return None + + def get_methylation_details(self, methylation_site: str) -> Optional[Dict]: + """Get methylation site details from Modulector SDK.""" + try: + return modulector.get_methylation_details( + methylation_site=methylation_site, + base_url=self._modulector_base_url + ) + except Exception as ex: + logging.exception(ex) + return None + + def get_mirna_target_interactions( + self, + mirna: Optional[str] = None, + gene: Optional[str] = None, + score: Optional[str] = None, + include_pubmeds: bool = False, + page: Optional[int] = None, + page_size: Optional[int] = None, + ) -> Dict: + """Get miRNA-target interactions from Modulector SDK.""" + try: + result = modulector.get_mirna_target_interactions( + mirna=mirna, + gene=gene, + score=float(score) if score else None, + include_pubmeds=include_pubmeds, + page=page, + page_size=page_size, + base_url=self._modulector_base_url, + ) + return self._paginated_to_dict(result) + except Exception as ex: + logging.exception(ex) + return self._paginated_to_dict(None) + + def get_diseases(self, mirna: Optional[str] = None, page: Optional[int] = None, + page_size: Optional[int] = None) -> Dict: + """Get miRNA disease associations from Modulector SDK.""" + try: + result = modulector.get_diseases( + mirna=mirna, + page=page, + page_size=page_size, + base_url=self._modulector_base_url, + ) + return self._paginated_to_dict(result) + except Exception as ex: + logging.exception(ex) + return self._paginated_to_dict(None) + + def get_drugs(self, mirna: Optional[str] = None, page: Optional[int] = None, + page_size: Optional[int] = None) -> Dict: + """Get drug/miRNA associations from Modulector SDK.""" + try: + result = modulector.get_drugs( + mirna=mirna, + page=page, + page_size=page_size, + base_url=self._modulector_base_url, + ) + return self._paginated_to_dict(result) + except Exception as ex: + logging.exception(ex) + return self._paginated_to_dict(None) + + def get_mirna_codes(self, mirna_codes: List[str]) -> Optional[Dict[str, Optional[str]]]: + """Resolve miRNA identifiers to standard codes via Modulector SDK.""" + try: + return modulector.get_mirna_codes( + mirna_codes=mirna_codes, + base_url=self._modulector_base_url, + ) + except Exception as ex: + logging.exception(ex) + return None + + def find_mirna_codes(self, query: str, limit: Optional[int] = None) -> List[str]: + """Search miRNA identifiers via Modulector SDK.""" + try: + kwargs: Dict[str, Any] = {'query': query, 'base_url': self._modulector_base_url} + if limit is not None: + kwargs['limit'] = limit + return modulector.find_mirna_codes(**kwargs) + except Exception as ex: + logging.exception(ex) + return [] + + def get_methylation_sites(self, methylation_sites: List[str]) -> Optional[Dict[str, List[str]]]: + """Resolve methylation site identifiers to EPIC 2.0 names via Modulector SDK.""" + try: + return modulector.get_methylation_sites( + methylation_sites=methylation_sites, + base_url=self._modulector_base_url, + ) + except Exception as ex: + logging.exception(ex) + return None + + def find_methylation_sites(self, query: str, limit: Optional[int] = None) -> List[str]: + """Search methylation site identifiers via Modulector SDK.""" + try: + kwargs: Dict[str, Any] = {'query': query, 'base_url': self._modulector_base_url} + if limit is not None: + kwargs['limit'] = limit + return modulector.find_methylation_sites(**kwargs) + except Exception as ex: + logging.exception(ex) + return [] -global_mrna_service = MRNAService() +global_mrna_service = MRNAService() \ No newline at end of file diff --git a/src/api_service/views.py b/src/api_service/views.py index 901da96b..0f0a3c6b 100644 --- a/src/api_service/views.py +++ b/src/api_service/views.py @@ -914,21 +914,23 @@ def get_number_samples_in_common_action_one_front(request): @login_required def mirna_data_action(request): """Gets miRNA data from Modulector""" - data = global_mrna_service.get_modulector_service_content('mirna', request.GET, is_paginated=False) + data = global_mrna_service.get_mirna_details(mirna=request.GET.get('mirna', '')) return generate_json_response_or_404(data) @login_required def methylation_data_action(request): """Gets Methylation site data from Modulector""" - data = global_mrna_service.get_modulector_service_content('methylation', request.GET, is_paginated=False) + data = global_mrna_service.get_methylation_details( + methylation_site=request.GET.get('methylation_site', '') + ) return generate_json_response_or_404(data) @login_required def get_mirna_target_interaction_action(request): """ - Searches in papers an specific miRNA interaction. + Searches in papers an specific miRNA interaction. Examples: http://127.0.0.1:8000/api-service/mirna-target-interaction?gene=BRCA1&mirna=hsa-miR-132-3p&include_pubmeds=true @@ -939,48 +941,40 @@ def get_mirna_target_interaction_action(request): """ gene = request.GET.get('gene') mirna = request.GET.get('mirna') - include_pubmeds = request.GET.get('include_pubmeds') if not gene and not mirna: return JsonResponse(data={"error": "Param 'mirna' or 'gene' are mandatory"}, status=400) - if gene and not mirna: - params = {'gene': gene} - elif not gene and mirna: - params = {'mirna': mirna} - else: - params = {'mirna': mirna, 'gene': gene} - - score = request.GET.get('score') - if score: - params['score'] = score - - if include_pubmeds: - if include_pubmeds.lower() == "true": - params['include_pubmeds'] = "true" - - # Gets include_pubmeds flag - include_pubmeds = request.GET.get('include_pubmeds') == 'true' - if include_pubmeds: - params['include_pubmeds'] = 'true' - data = global_mrna_service.get_modulector_service_content('mirna-target-interactions', - request_params=params, - is_paginated=True, - method='get') + data = global_mrna_service.get_mirna_target_interactions( + mirna=mirna, + gene=gene, + score=request.GET.get('score'), + include_pubmeds=request.GET.get('include_pubmeds', '').lower() == 'true', + page=request.GET.get('page'), + page_size=request.GET.get('page_size'), + ) return JsonResponse(data) @login_required def get_mirna_diseases_action(request): """Searches in papers miRNA associations with diseases""" - data = global_mrna_service.get_modulector_service_content('diseases', request.GET, is_paginated=True) + data = global_mrna_service.get_diseases( + mirna=request.GET.get('mirna'), + page=request.GET.get('page'), + page_size=request.GET.get('page_size'), + ) return generate_json_response_or_404(data) @login_required def get_mirna_drugs_action(request): """Searches in papers miRNA associations with drugs""" - data = global_mrna_service.get_modulector_service_content('drugs', request.GET, is_paginated=True) + data = global_mrna_service.get_drugs( + mirna=request.GET.get('mirna'), + page=request.GET.get('page'), + page_size=request.GET.get('page_size'), + ) return generate_json_response_or_404(data) diff --git a/src/assistant/services/tools.py b/src/assistant/services/tools.py index be3331d3..413457e1 100644 --- a/src/assistant/services/tools.py +++ b/src/assistant/services/tools.py @@ -542,15 +542,9 @@ def get_mirna_modulators(gene_name: str, min_score: float = 0.0, limit: int = 20 """ from api_service.mrna_service import global_mrna_service - params: dict = {'gene': gene_name} - if min_score > 0: - params['score'] = str(min_score) - - data = global_mrna_service.get_mdulector_service_content( - 'mirna-target-interactions', - request_params=params, - is_paginated=True, - method='get' + data = global_mrna_service.get_mirna_target_interactions( + gene=gene_name, + score=str(min_score) if min_score > 0 else None, ) if not data or data.get('count', 0) == 0: @@ -567,6 +561,122 @@ def get_mirna_modulators(gene_name: str, min_score: float = 0.0, limit: int = 20 'modulators': results }, default=str) + @tool + def search_mirna(query: str) -> str: + """ + Searches for miRNA identifiers in Modulector by name or partial name. + Use this when the user asks to search, find, or look up a miRNA by name + (e.g. "hsa-miR-132", "miR-21"). Returns a list of matching miRNA codes + and their standard accession IDs. + Always use this before get_mirna_details to confirm the exact identifier. + """ + from api_service.mrna_service import global_mrna_service + + results = global_mrna_service.find_mirna_codes(query=query) + + if not results: + return json.dumps({'message': f'No miRNAs found matching "{query}" in Modulector.'}) + + aliases = global_mrna_service.get_mirna_codes(mirna_codes=results) or {} + data = [{'molecule': m, 'standard': aliases.get(m)} for m in results] + return json.dumps({'query': query, 'count': len(data), 'results': data}, default=str) + + @tool + def get_mirna_details(mirna: str) -> str: + """ + Returns detailed information about a specific miRNA from Modulector, + including its accession ID, sequence, aliases, and database references. + Use this with an exact miRNA identifier obtained from search_mirna. + """ + from api_service.mrna_service import global_mrna_service + + data = global_mrna_service.get_mirna_details(mirna=mirna) + + if not data: + return json.dumps({'message': f'No information found for miRNA "{mirna}" in Modulector.'}) + + return json.dumps({'mirna': mirna, 'details': data}, default=str) + + @tool + def get_mirna_target_genes(mirna: str, min_score: float = 0.0, limit: int = 20) -> str: + """ + Returns genes known to be targeted/regulated by a specific miRNA, + sourced from Modulector's miRNA-target interaction database. + Use this when the user asks which genes a miRNA regulates, targets, + or silences, or asks about downstream targets of a miRNA. + Results include gene name, interaction score, and supporting evidence. + """ + from api_service.mrna_service import global_mrna_service + + data = global_mrna_service.get_mirna_target_interactions( + mirna=mirna, + score=str(min_score) if min_score > 0 else None, + ) + + if not data or data.get('count', 0) == 0: + return json.dumps({ + 'message': f'No target genes found for miRNA "{mirna}" in Modulector.', + 'results': [] + }) + + results = data.get('results', [])[:limit] + return json.dumps({ + 'mirna': mirna, + 'total_interactions': data.get('count', 0), + 'shown': len(results), + 'targets': results + }, default=str) + + @tool + def get_mirna_diseases(mirna: str, limit: int = 20) -> str: + """ + Returns diseases associated with a specific miRNA from Modulector. + Use this when the user asks about the clinical relevance, pathological + associations, or disease context of a miRNA. + """ + from api_service.mrna_service import global_mrna_service + + data = global_mrna_service.get_diseases(mirna=mirna) + + if not data or data.get('count', 0) == 0: + return json.dumps({ + 'message': f'No disease associations found for miRNA "{mirna}" in Modulector.', + 'results': [] + }) + + results = data.get('results', [])[:limit] + return json.dumps({ + 'mirna': mirna, + 'total': data.get('count', 0), + 'shown': len(results), + 'diseases': results + }, default=str) + + @tool + def get_mirna_drugs(mirna: str, limit: int = 20) -> str: + """ + Returns drugs or molecules associated with a specific miRNA from Modulector. + Use this when the user asks about pharmacological context, drug interactions, + or therapeutic relevance of a miRNA. + """ + from api_service.mrna_service import global_mrna_service + + data = global_mrna_service.get_drugs(mirna=mirna) + + if not data or data.get('count', 0) == 0: + return json.dumps({ + 'message': f'No drug associations found for miRNA "{mirna}" in Modulector.', + 'results': [] + }) + + results = data.get('results', [])[:limit] + return json.dumps({ + 'mirna': mirna, + 'total': data.get('count', 0), + 'shown': len(results), + 'drugs': results + }, default=str) + @tool def get_gene_annotations(gene_name: str) -> str: """ @@ -632,6 +742,11 @@ def get_drugs_regulating_gene(gene_name: str) -> str: get_gene_info, get_gene_annotations, get_mirna_modulators, + search_mirna, + get_mirna_details, + get_mirna_target_genes, + get_mirna_diseases, + get_mirna_drugs, get_drugs_regulating_gene, get_string_interaction_partners, get_string_functional_enrichment, diff --git a/src/biomarkers/views.py b/src/biomarkers/views.py index c67d9324..67c0ea54 100644 --- a/src/biomarkers/views.py +++ b/src/biomarkers/views.py @@ -168,6 +168,8 @@ def find_genes_from_request(request: Request) -> List[Dict]: """ genes_found = global_mrna_service.get_bioapi_service_content('gene-symbols-finder', request.GET, is_paginated=False) + if not genes_found: + return [] aliases = get_gene_aliases(genes_found) return [{'molecule': gene, 'standard': aliases.get(gene, [None])[0]} for gene in genes_found] @@ -197,17 +199,11 @@ class MiRNACodes(APIView): @staticmethod def __get_mirna_aliases(mirna_codes: List[str]) -> Optional[Dict]: """Get the aliases for a list of miRNAs through Modulector""" - return global_mrna_service.get_modulector_service_content( - 'mirna-codes', - request_params={'mirna_codes': mirna_codes}, - is_paginated=False, - method='post' - ) + return global_mrna_service.get_mirna_codes(mirna_codes=mirna_codes) def get(self, request): """Generates a query to search miRNAs through Modulector""" - mirnas_found = global_mrna_service.get_modulector_service_content('mirna-codes-finder', - request.GET, is_paginated=False) + mirnas_found = global_mrna_service.find_mirna_codes(query=request.GET.get('query', '')) # Generates the structure for the frontend aliases = self.__get_mirna_aliases(mirnas_found) @@ -235,23 +231,12 @@ class MethylationSites(APIView): @staticmethod def __get_methylation_sites_aliases(methylation_sites: List[str]) -> Optional[Dict]: """Get the aliases for a list of Methylation sites through Modulector""" - return global_mrna_service.get_modulector_service_content( - 'methylation-sites', - request_params={'methylation_sites': methylation_sites}, - is_paginated=False, - method='post' - ) + return global_mrna_service.get_methylation_sites(methylation_sites=methylation_sites) def get(self, request: Request): """Generates a query to search Methylation sites through Modulector""" - # methylation_sites = request.GET.get('methylation_sites', '') - # Gets the Methylation sites - sites_found = global_mrna_service.get_modulector_service_content( - 'methylation-sites-finder', - request_params=request.GET, - is_paginated=False - ) + sites_found = global_mrna_service.find_methylation_sites(query=request.GET.get('query', '')) # Generates the structure for the frontend aliases = self.__get_methylation_sites_aliases(sites_found) diff --git a/src/molecules_details/views.py b/src/molecules_details/views.py index 1d4c9c13..7fc61404 100644 --- a/src/molecules_details/views.py +++ b/src/molecules_details/views.py @@ -594,14 +594,7 @@ def get(request: HttpRequest): if not methylation_site: return Response(status=400, data={"error": "Param 'methylation_site' is mandatory"}) - data = global_mrna_service.get_modulector_service_content( - 'methylation', - request_params={ - 'methylation_site': methylation_site - }, - is_paginated=False, - method='get' - ) + data = global_mrna_service.get_methylation_details(methylation_site=methylation_site) return Response({ 'data': data if data else None diff --git a/src/multiomics_intermediate/settings.py b/src/multiomics_intermediate/settings.py index 52f39191..5f4737df 100644 --- a/src/multiomics_intermediate/settings.py +++ b/src/multiomics_intermediate/settings.py @@ -304,11 +304,15 @@ EMAIL_PAGE_DOMAIN = 'https://multiomix.org' # Modulector settings -MODULECTOR_SETTINGS = { - 'host': os.getenv('MODULECTOR_HOST', 'modulector.multiomix.org'), - 'port': os.getenv('MODULECTOR_PORT', 443), - 'protocol': os.getenv('BIOAPI_PROTOCOL', 'https') -} +_modulector_protocol = os.getenv('MODULECTOR_PROTOCOL', os.getenv('BIOAPI_PROTOCOL', 'https')) +_modulector_host = os.getenv('MODULECTOR_HOST', 'modulector.multiomix.org') +_modulector_port = os.getenv('MODULECTOR_PORT', '443') +# MODULECTOR_API_BASE_URL is also read directly by the modulector-sdk as env var +MODULECTOR_BASE_URL = os.getenv( + 'MODULECTOR_API_BASE_URL', + f"{_modulector_protocol}://{_modulector_host}" if _modulector_port in ('80', '443') + else f"{_modulector_protocol}://{_modulector_host}:{_modulector_port}" +) # BioAPI settings BIOAPI_SETTINGS = { From 83094adaa8852a9f50eb679460db73a2fb0a7ee5 Mon Sep 17 00:00:00 2001 From: Hernan Date: Thu, 30 Jul 2026 20:46:06 -0300 Subject: [PATCH 2/4] Move miRNA tools to Modulector MCP server Enable a dedicated `modulector` MCP server in `mcp_servers.json` and remove the in-process miRNA helper tools from `tools.py` (search/details/targets/diseases/drugs), along with their registration. This centralizes miRNA and related biological lookups through the external Modulector MCP integration instead of local assistant wrappers. --- config/mcp_servers.json | 10 +++ src/assistant/services/tools.py | 121 -------------------------------- 2 files changed, 10 insertions(+), 121 deletions(-) diff --git a/config/mcp_servers.json b/config/mcp_servers.json index 53313b39..b16263e7 100644 --- a/config/mcp_servers.json +++ b/config/mcp_servers.json @@ -8,6 +8,16 @@ "command": "biomcp-cli", "args": ["mcp"], "env": {} + }, + "modulector": { + "enabled": true, + "description": "miRNA target interactions, aliases, methylation sites, disease and drug associations", + "transport": "stdio", + "command": "modulector-mcp", + "args": [], + "env": { + "MODULECTOR_API_BASE_URL": "https://modulector.multiomix.org" + } } } } diff --git a/src/assistant/services/tools.py b/src/assistant/services/tools.py index 413457e1..39b7997e 100644 --- a/src/assistant/services/tools.py +++ b/src/assistant/services/tools.py @@ -561,122 +561,6 @@ def get_mirna_modulators(gene_name: str, min_score: float = 0.0, limit: int = 20 'modulators': results }, default=str) - @tool - def search_mirna(query: str) -> str: - """ - Searches for miRNA identifiers in Modulector by name or partial name. - Use this when the user asks to search, find, or look up a miRNA by name - (e.g. "hsa-miR-132", "miR-21"). Returns a list of matching miRNA codes - and their standard accession IDs. - Always use this before get_mirna_details to confirm the exact identifier. - """ - from api_service.mrna_service import global_mrna_service - - results = global_mrna_service.find_mirna_codes(query=query) - - if not results: - return json.dumps({'message': f'No miRNAs found matching "{query}" in Modulector.'}) - - aliases = global_mrna_service.get_mirna_codes(mirna_codes=results) or {} - data = [{'molecule': m, 'standard': aliases.get(m)} for m in results] - return json.dumps({'query': query, 'count': len(data), 'results': data}, default=str) - - @tool - def get_mirna_details(mirna: str) -> str: - """ - Returns detailed information about a specific miRNA from Modulector, - including its accession ID, sequence, aliases, and database references. - Use this with an exact miRNA identifier obtained from search_mirna. - """ - from api_service.mrna_service import global_mrna_service - - data = global_mrna_service.get_mirna_details(mirna=mirna) - - if not data: - return json.dumps({'message': f'No information found for miRNA "{mirna}" in Modulector.'}) - - return json.dumps({'mirna': mirna, 'details': data}, default=str) - - @tool - def get_mirna_target_genes(mirna: str, min_score: float = 0.0, limit: int = 20) -> str: - """ - Returns genes known to be targeted/regulated by a specific miRNA, - sourced from Modulector's miRNA-target interaction database. - Use this when the user asks which genes a miRNA regulates, targets, - or silences, or asks about downstream targets of a miRNA. - Results include gene name, interaction score, and supporting evidence. - """ - from api_service.mrna_service import global_mrna_service - - data = global_mrna_service.get_mirna_target_interactions( - mirna=mirna, - score=str(min_score) if min_score > 0 else None, - ) - - if not data or data.get('count', 0) == 0: - return json.dumps({ - 'message': f'No target genes found for miRNA "{mirna}" in Modulector.', - 'results': [] - }) - - results = data.get('results', [])[:limit] - return json.dumps({ - 'mirna': mirna, - 'total_interactions': data.get('count', 0), - 'shown': len(results), - 'targets': results - }, default=str) - - @tool - def get_mirna_diseases(mirna: str, limit: int = 20) -> str: - """ - Returns diseases associated with a specific miRNA from Modulector. - Use this when the user asks about the clinical relevance, pathological - associations, or disease context of a miRNA. - """ - from api_service.mrna_service import global_mrna_service - - data = global_mrna_service.get_diseases(mirna=mirna) - - if not data or data.get('count', 0) == 0: - return json.dumps({ - 'message': f'No disease associations found for miRNA "{mirna}" in Modulector.', - 'results': [] - }) - - results = data.get('results', [])[:limit] - return json.dumps({ - 'mirna': mirna, - 'total': data.get('count', 0), - 'shown': len(results), - 'diseases': results - }, default=str) - - @tool - def get_mirna_drugs(mirna: str, limit: int = 20) -> str: - """ - Returns drugs or molecules associated with a specific miRNA from Modulector. - Use this when the user asks about pharmacological context, drug interactions, - or therapeutic relevance of a miRNA. - """ - from api_service.mrna_service import global_mrna_service - - data = global_mrna_service.get_drugs(mirna=mirna) - - if not data or data.get('count', 0) == 0: - return json.dumps({ - 'message': f'No drug associations found for miRNA "{mirna}" in Modulector.', - 'results': [] - }) - - results = data.get('results', [])[:limit] - return json.dumps({ - 'mirna': mirna, - 'total': data.get('count', 0), - 'shown': len(results), - 'drugs': results - }, default=str) - @tool def get_gene_annotations(gene_name: str) -> str: """ @@ -742,11 +626,6 @@ def get_drugs_regulating_gene(gene_name: str) -> str: get_gene_info, get_gene_annotations, get_mirna_modulators, - search_mirna, - get_mirna_details, - get_mirna_target_genes, - get_mirna_diseases, - get_mirna_drugs, get_drugs_regulating_gene, get_string_interaction_partners, get_string_functional_enrichment, From 3b5efd6dd6b07f6b0c0bac695aaf01dfb8b9b6fd Mon Sep 17 00:00:00 2001 From: Hernan Date: Thu, 17 Sep 2026 20:11:59 -0300 Subject: [PATCH 3/4] Replace direct BioAPI HTTP calls with bioapi-sdk integration Replace custom BioAPI request logic with bioapi-sdk wrappers across services and views to simplify interactions and maintain consistency. Deprecate internal BioAPI request helpers and remove their usage across the codebase. Update requirements.txt to include bioapi-sdk as a dependency. --- config/requirements.txt | 3 +- src/api_service/mongo_service.py | 8 +- src/api_service/mrna_service.py | 244 +++++++++++++++--------- src/assistant/services/tools.py | 14 +- src/biomarkers/views.py | 14 +- src/multiomics_intermediate/settings.py | 8 +- 6 files changed, 168 insertions(+), 123 deletions(-) diff --git a/config/requirements.txt b/config/requirements.txt index 1a3603de..ae5c988c 100644 --- a/config/requirements.txt +++ b/config/requirements.txt @@ -39,4 +39,5 @@ pgvector==0.4.2 sentence-transformers==3.0.0 openai==2.38.0 biomcp-cli==0.8.22 -modulector-sdk==2.4.0 \ No newline at end of file +modulector-sdk==2.6.1 +bioapi-sdk==0.1.0 \ No newline at end of file diff --git a/src/api_service/mongo_service.py b/src/api_service/mongo_service.py index cb8ac047..3bba4a5e 100644 --- a/src/api_service/mongo_service.py +++ b/src/api_service/mongo_service.py @@ -214,13 +214,9 @@ def __get_standard_ids(file_type: FileType, molecules: List[str]) -> Optional[Di elif file_type == FileType.MIRNA: logging.warning('Retrieving data from Modulector') + data: Optional[Dict] if is_for_bioapi: - data = global_mrna_service.get_bioapi_service_content( - 'gene-symbols', - request_params={'gene_ids': molecules}, - is_paginated=False, - method='post' - ) + data = global_mrna_service.get_gene_symbols(gene_ids=molecules) elif file_type == FileType.MIRNA: data = global_mrna_service.get_mirna_codes(mirna_codes=molecules) else: diff --git a/src/api_service/mrna_service.py b/src/api_service/mrna_service.py index 04ec4910..97690814 100644 --- a/src/api_service/mrna_service.py +++ b/src/api_service/mrna_service.py @@ -1,11 +1,16 @@ import logging -from json.decoder import JSONDecodeError -from typing import Any, Dict, List, Optional, Literal, Union -import requests +from typing import Any, cast, Dict, List, Optional from django.conf import settings -from django.http import QueryDict -from requests.exceptions import ConnectionError +import bioapi_sdk +from bioapi_sdk import ( + CorrectionMethod, + GeneTermRelationType, + GeneTermsFilterType, + OntologyType, + PathwaySource, + TermRelationType, +) import modulector_sdk as modulector from modulector_sdk import PaginatedResponse @@ -37,85 +42,6 @@ def __build_url(svc_settings: Dict[str, Any]) -> str: else: return f"{protocol}://{host}:{port}" - @staticmethod - def __generate_rest_query_params(get_request: QueryDict) -> str: - """ - Generates a string with all the query params from GET request. - @param get_request: GET request with query params to send to DRF backend - @return: String to send to BioAPI - """ - return '&'.join([f'{key}={value}' for (key, value) in get_request.items()]) - - def __get_bioapi_content( - self, - service_name: str, - request_params: QueryDict, - is_paginated: bool, - method: Literal['get', 'post'], - ) -> Optional[Union[Dict, str]]: - """ - Generic function to make a request to a BioAPI service. - @param service_name: BioAPI service to consume - @param request_params: GET/POST request with query params - @param is_paginated: True if the expected response is paginated - @param method: Request method (GET or POST) - @return: JSON data retrieved from BioAPI. None if response has 404 status code - """ - url = f'{self.url_bioapi_prefix}/{service_name}/' - - data = None # Prevents Mypy warning - try: - if method == 'get': - params = self.__generate_rest_query_params(request_params) - if params: - url += f'/?{params}/' - data = requests.get(url) - else: - data = requests.post(url, json=request_params) - - if data.status_code != 200: - logging.warning(f'{method.upper()} to {url} returned status_code {data.status_code} and ' - f'message: {data.content}') - return None - - content_type = data.headers.get('Content-Type', '') - if 'application/json' in content_type: - return data.json() - elif 'text/plain' in content_type or 'text/html' in content_type: - return data.text - else: - return None - - except (ConnectionError, JSONDecodeError) as ex: - logging.error(f'Received data from BioAPI: {data}') - logging.exception(ex) - - if is_paginated: - return { - 'count': 0, - 'next': '', - 'previous': '', - 'results': [] - } - return None - - def get_bioapi_service_content( - self, - service_name: str, - request_params: QueryDict, - is_paginated: bool, - method: Literal['get', 'post'] = 'get' - ) -> Optional[Any]: - """ - Makes a request to a BioAPI service. - @param service_name: BioAPI service to consume - @param request_params: GET/POST params with query params to send to DRF backend - @param is_paginated: True if the expected response is paginated - @param method: Request method (GET or POST) - @return: JSON data retrieved from BioAPI. None if response has 404 status code - """ - return self.__get_bioapi_content(service_name, request_params, is_paginated, method) - # ------------------------------------------------------------------ # # Modulector SDK wrappers # # ------------------------------------------------------------------ # @@ -219,10 +145,7 @@ def get_mirna_codes(self, mirna_codes: List[str]) -> Optional[Dict[str, Optional def find_mirna_codes(self, query: str, limit: Optional[int] = None) -> List[str]: """Search miRNA identifiers via Modulector SDK.""" try: - kwargs: Dict[str, Any] = {'query': query, 'base_url': self._modulector_base_url} - if limit is not None: - kwargs['limit'] = limit - return modulector.find_mirna_codes(**kwargs) + return modulector.find_mirna_codes(query=query, limit=limit, base_url=self._modulector_base_url) except Exception as ex: logging.exception(ex) return [] @@ -241,13 +164,150 @@ def get_methylation_sites(self, methylation_sites: List[str]) -> Optional[Dict[s def find_methylation_sites(self, query: str, limit: Optional[int] = None) -> List[str]: """Search methylation site identifiers via Modulector SDK.""" try: - kwargs: Dict[str, Any] = {'query': query, 'base_url': self._modulector_base_url} - if limit is not None: - kwargs['limit'] = limit - return modulector.find_methylation_sites(**kwargs) + return modulector.find_methylation_sites(query=query, limit=limit, base_url=self._modulector_base_url) except Exception as ex: logging.exception(ex) return [] + # ------------------------------------------------------------------ # + # BioAPI SDK wrappers # + # ------------------------------------------------------------------ # + + def get_gene_symbols(self, gene_ids: List[str]) -> Optional[Dict[str, List[str]]]: + """Validate gene identifiers and get their HGNC-approved symbols via BioAPI SDK.""" + try: + return bioapi_sdk.gene_symbols(gene_ids=gene_ids, base_url=self.url_bioapi_prefix) + except Exception as ex: + logging.exception(ex) + return None + + def find_gene_symbols(self, query: str, limit: int = 50) -> List[str]: + """Search gene symbols via BioAPI SDK.""" + try: + return bioapi_sdk.gene_symbols_finder(query=query, limit=limit, base_url=self.url_bioapi_prefix) + except Exception as ex: + logging.exception(ex) + return [] + + def get_gene_information(self, gene_ids: List[str]) -> Optional[Dict]: + """Get genomic and database information for genes via BioAPI SDK.""" + try: + return bioapi_sdk.information_of_genes(gene_ids=gene_ids, base_url=self.url_bioapi_prefix) + except Exception as ex: + logging.exception(ex) + return None + + def get_genes_of_its_group(self, gene_id: str) -> Optional[Dict]: + """Get the HGNC gene group information for a gene via BioAPI SDK.""" + try: + return bioapi_sdk.genes_of_its_group(gene_id=gene_id, base_url=self.url_bioapi_prefix) + except Exception as ex: + logging.exception(ex) + return None + + def get_pathways_in_common(self, gene_ids: List[str]) -> Optional[Dict]: + """Get metabolic pathways common to a list of genes via BioAPI SDK.""" + try: + return bioapi_sdk.pathways_in_common(gene_ids=gene_ids, base_url=self.url_bioapi_prefix) + except Exception as ex: + logging.exception(ex) + return None + + def get_pathway_genes(self, source: str, external_id: str) -> Optional[Dict]: + """Get genes involved in a metabolic pathway via BioAPI SDK.""" + try: + return bioapi_sdk.pathway_genes( + source=cast(PathwaySource, source), + external_id=external_id, + base_url=self.url_bioapi_prefix, + ) + except Exception as ex: + logging.exception(ex) + return None + + def get_genes_to_terms( + self, + gene_ids: List[str], + filter_type: str = 'intersection', + relation_type: Optional[List[str]] = None, + ontology_type: Optional[List[str]] = None, + p_value_threshold: Optional[float] = None, + correction_method: Optional[str] = None, + ) -> Optional[List[Dict]]: + """Get Gene Ontology terms related to a list of genes via BioAPI SDK.""" + try: + return bioapi_sdk.genes_to_terms( + gene_ids=gene_ids, + filter_type=cast(GeneTermsFilterType, filter_type), + relation_type=cast(Optional[List[GeneTermRelationType]], relation_type), + ontology_type=cast(Optional[List[OntologyType]], ontology_type), + p_value_threshold=p_value_threshold, + correction_method=cast(Optional[CorrectionMethod], correction_method), + base_url=self.url_bioapi_prefix, + ) + except Exception as ex: + logging.exception(ex) + return None + + def get_related_terms( + self, + term_id: str, + relations: Optional[List[str]] = None, + ontology_type: Optional[List[str]] = None, + general_depth: Optional[int] = None, + hierarchical_depth_to_children: Optional[int] = None, + to_root: Optional[bool] = None, + ) -> Optional[List[Dict]]: + """Get Gene Ontology terms related to a term via BioAPI SDK.""" + try: + return bioapi_sdk.related_terms( + term_id=term_id, + relations=cast(Optional[List[TermRelationType]], relations), + ontology_type=cast(Optional[List[OntologyType]], ontology_type), + general_depth=general_depth, + hierarchical_depth_to_children=hierarchical_depth_to_children, + to_root=to_root, + base_url=self.url_bioapi_prefix, + ) + except Exception as ex: + logging.exception(ex) + return None + + def get_oncokb_information(self, gene_ids: List[str], query: Optional[str] = None) -> Optional[Dict]: + """Get OncoKB cancer evidence and precision therapy data for genes via BioAPI SDK.""" + try: + return bioapi_sdk.information_of_oncokb(gene_ids=gene_ids, query=query, base_url=self.url_bioapi_prefix) + except Exception as ex: + logging.exception(ex) + return None + + def get_drugs_pharm_gkb(self, gene_ids: List[str]) -> Optional[Dict]: + """Get PharmGKB cancer-related drug labels for genes via BioAPI SDK.""" + try: + return bioapi_sdk.drugs_pharm_gkb(gene_ids=gene_ids, base_url=self.url_bioapi_prefix) + except Exception as ex: + logging.exception(ex) + return None + + def get_string_relations(self, gene_id: str, min_combined_score: Optional[int] = None) -> Optional[List[Dict]]: + """Get STRING functional association relations for a gene via BioAPI SDK.""" + try: + return bioapi_sdk.string_relations( + gene_id=gene_id, + min_combined_score=min_combined_score, + base_url=self.url_bioapi_prefix, + ) + except Exception as ex: + logging.exception(ex) + return None + + def get_drugs_regulating_gene(self, gene_id: str) -> Optional[Dict]: + """Get a DrugBank link listing drugs that regulate a gene via BioAPI SDK.""" + try: + return bioapi_sdk.drugs_regulating_gene(gene_id=gene_id, base_url=self.url_bioapi_prefix) + except Exception as ex: + logging.exception(ex) + return None + global_mrna_service = MRNAService() \ No newline at end of file diff --git a/src/assistant/services/tools.py b/src/assistant/services/tools.py index 39b7997e..fd0dae8b 100644 --- a/src/assistant/services/tools.py +++ b/src/assistant/services/tools.py @@ -572,12 +572,7 @@ def get_gene_annotations(gene_name: str) -> str: """ from api_service.mrna_service import global_mrna_service - data = global_mrna_service.get_bioapi_service_content( - 'information-of-genes', - request_params={'gene_ids': [gene_name]}, - is_paginated=False, - method='post' - ) + data = global_mrna_service.get_gene_information(gene_ids=[gene_name]) if not data: return json.dumps({'error': f'No annotation data found for gene "{gene_name}" in BioAPI.'}) @@ -594,12 +589,7 @@ def get_drugs_regulating_gene(gene_name: str) -> str: """ from api_service.mrna_service import global_mrna_service - data = global_mrna_service.get_bioapi_service_content( - f'/drugs-regulating-gene/{gene_name}', - request_params={}, - is_paginated=False, - method='get' - ) + data = global_mrna_service.get_drugs_regulating_gene(gene_id=gene_name) if not data or 'link' not in data: return json.dumps({ diff --git a/src/biomarkers/views.py b/src/biomarkers/views.py index 67c0ea54..1eabb64a 100644 --- a/src/biomarkers/views.py +++ b/src/biomarkers/views.py @@ -153,12 +153,7 @@ def biomarkers_action(request): def get_gene_aliases(genes_ids: List[str]) -> Optional[Dict]: """Get the aliases for a list of genes from BioAPI""" - return global_mrna_service.get_bioapi_service_content( - 'gene-symbols', - request_params={'gene_ids': genes_ids}, - is_paginated=False, - method='post' - ) + return global_mrna_service.get_gene_symbols(gene_ids=genes_ids) def find_genes_from_request(request: Request) -> List[Dict]: @@ -166,8 +161,11 @@ def find_genes_from_request(request: Request) -> List[Dict]: Generates the structure for the frontend for a list of genes. The needed structure is a list of dicts with the following keys: molecule, standard """ - genes_found = global_mrna_service.get_bioapi_service_content('gene-symbols-finder', - request.GET, is_paginated=False) + limit = request.GET.get('limit') + genes_found = global_mrna_service.find_gene_symbols( + query=request.GET.get('query', ''), + limit=int(limit) if limit else 50, + ) if not genes_found: return [] aliases = get_gene_aliases(genes_found) diff --git a/src/multiomics_intermediate/settings.py b/src/multiomics_intermediate/settings.py index 5f4737df..f558fdd8 100644 --- a/src/multiomics_intermediate/settings.py +++ b/src/multiomics_intermediate/settings.py @@ -304,13 +304,13 @@ EMAIL_PAGE_DOMAIN = 'https://multiomix.org' # Modulector settings -_modulector_protocol = os.getenv('MODULECTOR_PROTOCOL', os.getenv('BIOAPI_PROTOCOL', 'https')) -_modulector_host = os.getenv('MODULECTOR_HOST', 'modulector.multiomix.org') -_modulector_port = os.getenv('MODULECTOR_PORT', '443') +_modulector_protocol: str = os.getenv('MODULECTOR_PROTOCOL', os.getenv('BIOAPI_PROTOCOL', 'https')) +_modulector_host: str = os.getenv('MODULECTOR_HOST', 'modulector.multiomix.org') +_modulector_port: int = int(os.getenv('MODULECTOR_PORT', '443')) # MODULECTOR_API_BASE_URL is also read directly by the modulector-sdk as env var MODULECTOR_BASE_URL = os.getenv( 'MODULECTOR_API_BASE_URL', - f"{_modulector_protocol}://{_modulector_host}" if _modulector_port in ('80', '443') + f"{_modulector_protocol}://{_modulector_host}" if _modulector_port in (80, 443) else f"{_modulector_protocol}://{_modulector_host}:{_modulector_port}" ) From a03fdd10a119e4e7c6fdc5793e15adf529cf8af0 Mon Sep 17 00:00:00 2001 From: Hernan Date: Thu, 17 Sep 2026 20:16:34 -0300 Subject: [PATCH 4/4] Replace direct BioAPI HTTP calls with bioapi-sdk integration Replace custom BioAPI request logic with bioapi-sdk wrappers across services and views to simplify interactions and maintain consistency. Deprecate internal BioAPI request helpers and remove their usage across the codebase. Update requirements.txt to include bioapi-sdk as a dependency. --- src/molecules_details/views.py | 217 +++++++++++---------------------- 1 file changed, 73 insertions(+), 144 deletions(-) diff --git a/src/molecules_details/views.py b/src/molecules_details/views.py index 7fc61404..4f499ea5 100644 --- a/src/molecules_details/views.py +++ b/src/molecules_details/views.py @@ -20,19 +20,11 @@ class GeneInformation(APIView): def get(request: HttpRequest): gene = request.GET.get('gene', '').strip() - if gene: - gene = gene.split(',') - else: + if not gene: return Response(status=400, data={"error": "Param 'gene' is mandatory"}) + gene_ids = gene.split(',') - data = global_mrna_service.get_bioapi_service_content( - 'information-of-genes', - request_params={ - 'gene_ids': gene - }, - is_paginated=False, - method='post' - ) + data = global_mrna_service.get_gene_information(gene_ids=gene_ids) return Response({ 'data': data if data else None @@ -56,12 +48,7 @@ def get(request: HttpRequest): if not gene: return Response(status=400, data={"error": "Param 'gene' is mandatory"}) - data = global_mrna_service.get_bioapi_service_content( - f'/genes-of-its-group/{gene}', - request_params={}, # No params needed - is_paginated=False, - method='get' - ) + data = global_mrna_service.get_genes_of_its_group(gene_id=gene) return Response({ 'data': data if data else None }) @@ -83,19 +70,11 @@ class PathwaysInformation(APIView): def get(request: HttpRequest): gene = request.GET.get('gene', '').strip() - if gene: - gene = gene.split(',') - else: + if not gene: return Response(status=400, data={"error": "Param 'gene' is mandatory"}) + gene_ids = gene.split(',') - data = global_mrna_service.get_bioapi_service_content( - 'pathways-in-common', - request_params={ - 'gene_ids': gene - }, - is_paginated=False, - method='post' - ) + data = global_mrna_service.get_pathways_in_common(gene_ids=gene_ids) return Response({ 'data': data['pathways'] if data and 'pathways' in data else None @@ -124,12 +103,7 @@ def get(request: HttpRequest): if not pathway_id: return Response(status=400, data={"error": "Param 'id' is mandatory"}) - data = global_mrna_service.get_bioapi_service_content( - f'/pathway-genes/{source}/{pathway_id}', - request_params={}, # No params needed - is_paginated=False, - method='get' - ) + data = global_mrna_service.get_pathway_genes(source=source, external_id=pathway_id) return Response({ 'data': data['genes'] if data and 'genes' in data else None }) @@ -150,17 +124,17 @@ def get(request: HttpRequest): filter_type = request.GET.get('filter_type', '').strip() p_value_threshold = request.GET.get('p_value_threshold', '').strip() correction_method = request.GET.get('correction_method', '').strip() - relation_type = request.GET.get('relation_type', '').strip() - ontology_type = request.GET.get('ontology_type', '').strip() + relation_type_param = request.GET.get('relation_type', '').strip() + ontology_type_param = request.GET.get('ontology_type', '').strip() - if gene: - gene = gene.split(',') - else: + if not gene: return Response(status=400, data={"error": "Param 'gene' is mandatory"}) + gene_ids = gene.split(',') if not filter_type: filter_type = 'intersection' + relation_types: list[str] if filter_type not in ["intersection", "union", "enrichment"]: return Response(status=400, data={"error": "The 'filter_type' parameter must be one of the following " "options: 'intersection', 'union' or 'enrichment'"}) @@ -172,56 +146,42 @@ def get(request: HttpRequest): if not correction_method: return Response(status=400, data={"error": "The 'correction_method' parameter is mandatory if " "'filter_type' is 'enrichment'"}) + relation_types = [] else: - if not relation_type: - relation_type = ["enables", "involved_in", - "part_of", "located_in"] + if not relation_type_param: + relation_types = ["enables", "involved_in", "part_of", "located_in"] else: - relation_type = relation_type.split(',') - for relation in relation_type: + relation_types = relation_type_param.split(',') + for relation in relation_types: if relation not in ["enables", "involved_in", "part_of", "located_in"]: return Response(status=400, data={"error": "The 'relation_type' parameter must be a " "combination of the following options: " "'enables', 'involved_in', 'part_of' and " "'located_in'"}) - if not ontology_type: - ontology_type = ["biological_process", - "molecular_function", "cellular_component"] + if not ontology_type_param: + ontology_types = ["biological_process", "molecular_function", "cellular_component"] else: - ontology_type = ontology_type.split(',') - for type_elem in ontology_type: + ontology_types = ontology_type_param.split(',') + for type_elem in ontology_types: if type_elem not in ["biological_process", "molecular_function", "cellular_component"]: return Response(status=400, data={"error": "The 'ontology_type' parameter must be a combination of " "the following options: 'biological_process', " "'molecular_function' and 'cellular_component'"}) if filter_type in ["intersection", "union"]: - data = global_mrna_service.get_bioapi_service_content( - 'genes-to-terms', - # request_params={ - # 'gene_ids': [gene], - # 'filter_type': filter_type, - # 'ontology_type': ontology_type, - # 'relation_type': relation_type - # }, - # TODO: remove this and uncomment the above code when the bioapi service is fixed - request_params={ - "gene_ids": ["TMCO4"], "relation_type": ["enables"], "ontology_type": ["molecular_function"] - }, - is_paginated=False, - method='post' + # TODO: use gene_ids=gene_ids, filter_type=filter_type, ontology_type=ontology_types, + # relation_type=relation_types when the bioapi service is fixed + data = global_mrna_service.get_genes_to_terms( + gene_ids=["TMCO4"], + relation_type=["enables"], + ontology_type=["molecular_function"], ) else: # filter_type == "enrichment" - data = global_mrna_service.get_bioapi_service_content( - 'genes-to-terms', - request_params={ - 'gene_ids': gene, - 'filter_type': filter_type, - 'p_value_threshold': p_value_threshold, - 'correction_method': correction_method, - 'ontology_type': ontology_type - }, - is_paginated=False, - method='post' + data = global_mrna_service.get_genes_to_terms( + gene_ids=gene_ids, + filter_type=filter_type, + p_value_threshold=float(p_value_threshold), + correction_method=correction_method, + ontology_type=ontology_types, ) return Response({ @@ -239,7 +199,7 @@ class GeneOntologyTermsOfTerm(APIView): permission_classes = [permissions.IsAuthenticated] @staticmethod - def __process_go_data(go_terms: dict) -> dict: + def __process_go_data(go_terms: list[dict] | None) -> dict: """ Generates a structure with the GO terms data in a format ready-to-use for cytoscape frontend lib. @param go_terms: BioAPI GO data. @@ -286,61 +246,57 @@ def get(self, request: HttpRequest): if not general_depth.isnumeric(): return Response(status=400, data={"error": "Param 'general_depth' must be a numeric value"}) - hierarchical_depth_to_children = request.GET.get('hierarchical_depth_to_children', '0').strip() - if hierarchical_depth_to_children: - if not hierarchical_depth_to_children.isnumeric(): + hierarchical_depth_to_children_param = request.GET.get('hierarchical_depth_to_children', '0').strip() + hierarchical_depth_to_children = 0 + if hierarchical_depth_to_children_param: + if not hierarchical_depth_to_children_param.isnumeric(): return Response(status=400, data={"error": "Param 'hierarchical_depth_to_children' must be a " "numeric value"}) - hierarchical_depth_to_children = int(hierarchical_depth_to_children) + hierarchical_depth_to_children = int(hierarchical_depth_to_children_param) - to_root = request.GET.get('to_root', '1').strip() - if to_root: - if to_root not in ["0", "1"]: + to_root_param = request.GET.get('to_root', '1').strip() + to_root = True + if to_root_param: + if to_root_param not in ["0", "1"]: return Response(status=400, data={"error": "Param 'to_root' must be '0' or '1'"}) - to_root = int(to_root) + to_root = bool(int(to_root_param)) - relations = request.GET.get('relations', '').strip() - if not relations: + relations_param = request.GET.get('relations', '').strip() + if not relations_param: relations = ["part_of", "regulates", "has_part"] else: - relations = relations.split(',') + relations = relations_param.split(',') for relation in relations: if relation not in ["part_of", "regulates", "has_part"]: return Response(status=400, data={"error": "The 'relations' parameter must be a combination of " "the following options: 'part_of', 'regulates' and " "'has_part'"}) - ontology_type = request.GET.get('ontology_type', '').strip() - if not ontology_type: - ontology_type = ["biological_process", - "molecular_function", "cellular_component"] + ontology_type_param = request.GET.get('ontology_type', '').strip() + if not ontology_type_param: + ontology_types = ["biological_process", "molecular_function", "cellular_component"] else: - ontology_type = ontology_type.split(',') - for type_elem in ontology_type: + ontology_types = ontology_type_param.split(',') + for type_elem in ontology_types: if type_elem not in ["biological_process", "molecular_function", "cellular_component"]: return Response(status=400, data={"error": "The 'ontology_type' parameter must be a combination " "of the following options: 'biological_process', " "'molecular_function' and 'cellular_component'"}) - data = global_mrna_service.get_bioapi_service_content( - 'related-terms', - request_params={ - 'term_id': term_id, - 'relations': relations, - 'ontology_type': ontology_type, - 'general_depth': int(general_depth), - 'hierarchical_depth_to_children': hierarchical_depth_to_children, - 'to_root': to_root - }, - is_paginated=False, - method='post' + data = global_mrna_service.get_related_terms( + term_id=term_id, + relations=relations, + ontology_type=ontology_types, + general_depth=int(general_depth), + hierarchical_depth_to_children=hierarchical_depth_to_children, + to_root=to_root, ) # Generates structure for cytoscape in frontend - data = self.__process_go_data(data) + go_terms_data = self.__process_go_data(data) return Response({ - 'go_terms': data + 'go_terms': go_terms_data }) @@ -363,21 +319,12 @@ def get(request: HttpRequest): if not gene: return Response(status=400, data={"error": "Param 'gene' is mandatory"}) - else: - gene = gene.split(',') + gene_ids = gene.split(',') if not query: query = "" - data = global_mrna_service.get_bioapi_service_content( - 'information-of-oncokb', - request_params={ - 'gene_ids': gene, - 'query': query - }, - is_paginated=False, - method='post' - ) + data = global_mrna_service.get_oncokb_information(gene_ids=gene_ids, query=query) return Response({ 'data': data if data else None @@ -398,17 +345,9 @@ def get(request: HttpRequest): gene = request.GET.get('gene', '').strip() if not gene: return Response(status=400, data={"error": "Param 'gene' is mandatory"}) - else: - gene = gene.split(',') + gene_ids = gene.split(',') - data = global_mrna_service.get_bioapi_service_content( - 'drugs-pharm-gkb', - request_params={ - 'gene_ids': gene - }, - is_paginated=False, - method='post' - ) + data = global_mrna_service.get_drugs_pharm_gkb(gene_ids=gene_ids) return Response({ 'data': data if data else None @@ -489,7 +428,7 @@ def __generate_edge(association: dict[str, Any], source_key: str, target_key: st 'grabbable': True, } - def __process_associations_data(self, associations: dict) -> list[dict]: + def __process_associations_data(self, associations: list[dict] | None) -> list[dict]: """ Generates a structure with the genes associations data in a format ready-to-use for cytoscape frontend lib. @param associations: BioAPI Gene data. @@ -536,20 +475,15 @@ def get(self, request: HttpRequest): else: return Response(status=400, data={"error": "Param 'min_combined_score' must be a numeric value"}) - data = global_mrna_service.get_bioapi_service_content( - 'string-relations', - request_params={ - 'gene_id': gene_id, - 'min_combined_score': max(900, int(min_combined_score)) - }, - is_paginated=False, - method='post' + data = global_mrna_service.get_string_relations( + gene_id=gene_id, + min_combined_score=max(900, int(min_combined_score)), ) # Generates structure for cytoscape in frontend - data = self.__process_associations_data(data) + associations_data = self.__process_associations_data(data) - return Response({'data': data}) + return Response({'data': associations_data}) class DrugsRegulatingGene(APIView): @@ -568,12 +502,7 @@ def get(request: HttpRequest): if not gene: return Response(status=400, data={"error": "Param 'gene' is mandatory"}) - data = global_mrna_service.get_bioapi_service_content( - f'/drugs-regulating-gene/{gene}', - request_params={}, # No params needed - is_paginated=False, - method='get' - ) + data = global_mrna_service.get_drugs_regulating_gene(gene_id=gene) return Response({ 'data': data["link"] if data and "link" in data else None })