| |
| from langchain_core.tools import BaseTool |
| from typing import Type, Optional |
| from pydantic import BaseModel, Field |
| |
| from clinical_nlp.umls_bioportal import search_bioportal_term |
| from services.logger import app_logger |
| from services.metrics import log_tool_usage |
|
|
| class BioPortalInput(BaseModel): |
| term: str = Field(description="The medical term to search for.") |
| ontology: Optional[str] = Field( |
| default="SNOMEDCT_US", |
| description=( |
| "The specific ontology acronym to search within BioPortal (e.g., SNOMEDCT_US, ICD10CM, RXNORM, MESH, LOINC, NCIT). " |
| "Defaults to SNOMEDCT_US if not specified." |
| ) |
| ) |
|
|
| class BioPortalLookupTool(BaseTool): |
| name: str = "bioportal_lookup" |
| description: str = ( |
| "Use this tool to search for medical terms, codes (like ICD-10, SNOMED CT, RxNorm codes), definitions, and concept details " |
| "across a wide range of biomedical ontologies via BioPortal. " |
| "Useful when you need information from a specific ontology or want to explore different coding systems. " |
| "Input is a dictionary with 'term' and optional 'ontology'. If ontology is not specified by the user, you should default to 'SNOMEDCT_US'." |
| ) |
| args_schema: Type[BaseModel] = BioPortalInput |
|
|
| def _run(self, term: str, ontology: Optional[str] = "SNOMEDCT_US") -> str: |
| |
| target_ontology = ontology or "SNOMEDCT_US" |
| app_logger.info(f"BioPortal Tool called with term: '{term}', ontology: '{target_ontology}'") |
| log_tool_usage(self.name, {"query_term": term, "ontology": target_ontology}) |
|
|
| if not term or not term.strip(): |
| return "Error from BioPortal lookup: No search term provided." |
|
|
| try: |
| results = search_bioportal_term(term, ontology=target_ontology) |
| except Exception as e: |
| app_logger.error(f"Exception during BioPortal search for '{term}' in '{target_ontology}': {e}", exc_info=True) |
| return f"Error performing BioPortal lookup for '{term}': An unexpected error occurred during the search." |
|
|
| if isinstance(results, dict) and "error" in results: |
| app_logger.warning(f"BioPortal lookup for '{term}' in '{target_ontology}' returned an error: {results['error']}") |
| return f"Error from BioPortal lookup for '{term}' (Ontology: {target_ontology}): {results['error']}" |
| |
| collection = results.get("collection", []) if isinstance(results, dict) else [] |
| if collection: |
| formatted_results = [] |
| for item in collection[:3]: |
| pref_label = item.get('prefLabel', 'N/A') |
| defs = item.get('definition', []) |
| definition_str = ("; ".join(d for d in defs if d) if defs else "No definition available.")[:200] + "..." |
| cui_list = item.get('cui', []) |
| cui_str = ", ".join(cui_list) if cui_list else "N/A" |
| |
| formatted_results.append( |
| f"- Term: {pref_label} (CUIs: {cui_str}). Definition: {definition_str}" |
| ) |
| if formatted_results: |
| return f"BioPortal Search Results for '{term}' (Ontology: {target_ontology}):\n" + "\n".join(formatted_results) |
| else: |
| return f"No specific items found in BioPortal results for '{term}' (Ontology: {target_ontology}), though the query was successful." |
|
|
| app_logger.warning(f"No results or unexpected format from BioPortal for '{term}' in '{target_ontology}'. Raw: {str(results)[:200]}") |
| return f"No results found in BioPortal for term '{term}' in ontology '{target_ontology}'." |
|
|
| async def _arun(self, term: str, ontology: Optional[str] = "SNOMEDCT_US") -> str: |
| |
| return self._run(term, ontology) |