Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions config/mcp_servers.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,19 @@
"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": {}
},
"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"
}
}
}
}
3 changes: 2 additions & 1 deletion config/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
biomcp-cli==0.8.22
modulector-sdk==2.4.0
7 changes: 1 addition & 6 deletions src/api_service/mongo_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
209 changes: 158 additions & 51 deletions src/api_service/mrna_service.py
Original file line number Diff line number Diff line change
@@ -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}"
Expand All @@ -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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Borrar, no debería usarse más

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:
Expand All @@ -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:
Expand All @@ -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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Borrar, no debería usarse más ahora que tenemos la SDK

self,
service_name: str,
Expand All @@ -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}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplificar, en vez de hacer un diccionario y después desconstruir:

return modulector.find_methylation_sites(query=query, limit=limit)

Aplica esta mejora a todas las funciones de este archivo

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()
Loading
Loading