import gradio as gr from typing import Dict, Any from datetime import datetime, timedelta from brand_to_generic import brand_lookup, set_pbs_data from dbi_mcp import dbi_mcp, dbi_mcp_mixed_routes from clinical_calculators import ( cockcroft_gault_creatinine_clearance, ckd_epi_egfr, child_pugh_score, bmi_calculator, ideal_body_weight, dosing_weight_recommendation, creatinine_conversion, ) from caching import with_caching, api_cache from utils import with_error_handling, standardize_response, format_json_output from drug_data_endpoints import ( search_adverse_events, fetch_event_details, drug_label_warnings, drug_recalls, drug_pregnancy_lactation, drug_dose_adjustments, drug_livertox_summary, ) from adr_analysis import ( enhanced_faers_search, calculate_naranjo_score, disproportionality_analysis, find_similar_cases, temporal_analysis, ) import time import sys import logging from apscheduler.schedulers.background import BackgroundScheduler import pandas as pd try: from datasets import load_dataset HAVE_DATASETS = True except ImportError: HAVE_DATASETS = False # Setup logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def load_pbs_data(): """Load PBS data from Hugging Face Hub, with fallback to previous month.""" if not HAVE_DATASETS: logger.warning("`datasets` library not installed. Skipping PBS data load.") set_pbs_data(pd.DataFrame()) return today = datetime.now() current_month_str = today.strftime("%Y-%m") first_day_current_month = today.replace(day=1) last_day_last_month = first_day_current_month - timedelta(days=1) last_month_str = last_day_last_month.strftime("%Y-%m") loaded = False for month_str in [current_month_str, last_month_str]: try: logger.info(f"Attempting to load PBS data for {month_str}") ds = load_dataset("cmcmaster/pbs_items", month_str, trust_remote_code=True) if 'train' in ds: pbs_df = ds['train'].to_pandas() set_pbs_data(pbs_df) logger.info(f"Successfully loaded PBS data for {month_str}. Shape: {pbs_df.shape}") loaded = True break else: logger.error(f"No 'train' split found in dataset for month {month_str}") except Exception as e: logger.warning(f"Failed to load PBS data for {month_str}: {e}") if not loaded: logger.error(f"Failed to load PBS data for both {current_month_str} and {last_month_str}. PBS lookups will be disabled.") set_pbs_data(pd.DataFrame()) # Initial load on startup logger.info("Performing initial load of PBS data...") load_pbs_data() # Schedule daily refresh scheduler = BackgroundScheduler(daemon=True) scheduler.add_job(load_pbs_data, 'interval', days=1) scheduler.start() @with_error_handling def _brand_lookup_gradio(brand_name: str, prefer_countries_str: str = ""): """Brand to generic lookup for single input.""" prefer_countries_list = ( [c.strip().upper() for c in prefer_countries_str.split(",") if c.strip()] if prefer_countries_str else None ) result = brand_lookup(brand_name, prefer_countries=prefer_countries_list) return standardize_response(result, "brand_to_generic") @with_error_handling def _dbi_mcp_mixed_routes_gradio(text_block: str): result = dbi_mcp_mixed_routes(text_block, ref_csv="dbi_reference_by_route.csv") return standardize_response(result, "dbi_calculator_mixed_routes") @with_error_handling def _cockcroft_gault_gradio( age: int, weight_kg: float, serum_creatinine: float, is_female: bool ): result = cockcroft_gault_creatinine_clearance( age, weight_kg, serum_creatinine, is_female ) return standardize_response(result, "cockcroft_gault_calculator") @with_error_handling def _ckd_epi_gradio(age: int, serum_creatinine: float, is_female: bool, is_black: bool): result = ckd_epi_egfr(age, serum_creatinine, is_female, is_black) return standardize_response(result, "ckd_epi_calculator") @with_error_handling def _child_pugh_gradio( bilirubin: float, albumin: float, inr: float, ascites: str, encephalopathy: str ): result = child_pugh_score(bilirubin, albumin, inr, ascites, encephalopathy) return standardize_response(result, "child_pugh_calculator") @with_error_handling def _bmi_gradio(weight_kg: float, height_cm: float): result = bmi_calculator(weight_kg, height_cm) return standardize_response(result, "bmi_calculator") @with_error_handling def _ideal_body_weight_gradio(height_cm: float, is_male: bool): result = ideal_body_weight(height_cm, is_male) return standardize_response(result, "ideal_body_weight_calculator") @with_error_handling def _dosing_weight_gradio(actual_weight: float, height_cm: float, is_male: bool): result = dosing_weight_recommendation(actual_weight, height_cm, is_male) return standardize_response(result, "dosing_weight_calculator") @with_error_handling def _creatinine_conversion_gradio(value: float, from_unit: str, to_unit: str): result = creatinine_conversion(value, from_unit, to_unit) return standardize_response(result, "creatinine_conversion") @with_error_handling @with_caching(ttl=1800) def search_adverse_events_mcp(drug_name: str, limit: str = "5") -> str: """ Searches the FDA Adverse Event Reporting System (FAERS) database for adverse events associated with a specific drug. This tool is useful for initial investigation into a drug's safety profile by retrieving summaries of reported adverse event cases. It provides a quick overview of potential side effects reported by healthcare professionals and the public. Args: drug_name (str): Generic or brand name to search (case-insensitive) limit (str): Maximum number of FAERS safety reports to return (default: "5") Returns: str: JSON string with adverse event contexts and metadata """ limit_int = int(limit) if limit.isdigit() else 5 result = search_adverse_events(drug_name, limit_int) return format_json_output(result) @with_error_handling @with_caching(ttl=3600) def fetch_event_details_mcp(event_id: str) -> str: """ Retrieves the complete details of a specific adverse event case from the FDA Adverse Event Reporting System (FAERS) using its unique safety report ID. Use this tool when you need to dive deep into a particular case found via 'search_adverse_events_mcp' to understand the full context, including patient demographics, concomitant medications, and the full narrative of the event. Args: event_id (str): Numeric FAERS safetyreportid string Returns: str: JSON string with structured case data including drugs, reactions, and full record """ result = fetch_event_details(event_id) return format_json_output(result) @with_error_handling @with_caching(ttl=7200) def drug_label_warnings_mcp(drug_name: str) -> str: """ Fetches critical safety information from the official FDA drug label. This includes boxed warnings (the most serious type), contraindications (situations where the drug should not be used), and known drug interactions. This is a primary tool for assessing a drug's major safety risks before prescribing or dispensing. Args: drug_name (str): Generic drug name preferred Returns: str: JSON string with boxed warnings, contraindications, and interaction data """ result = drug_label_warnings(drug_name) return format_json_output(result) @with_error_handling @with_caching(ttl=3600) def drug_recalls_mcp(drug_name: str, limit: str = "5") -> str: """ Searches for recent FDA-issued recall events for a specific drug product. This is critical for ensuring patient safety by identifying if a drug or specific batch has been recalled due to manufacturing defects, contamination, or other safety concerns. The results include details on the recall reason, status, and affected lots. Args: drug_name (str): Free-text search string for the drug limit (str): Maximum number of recall notices to return (default: "5") Returns: str: JSON string with recall notices including recall number, status, and reason """ limit_int = int(limit) if limit.isdigit() else 5 result = drug_recalls(drug_name, limit_int) return format_json_output(result) @with_error_handling @with_caching(ttl=7200) def drug_pregnancy_lactation_mcp(drug_name: str) -> str: """ Retrieves specific sections from the FDA drug label related to use during pregnancy and lactation. This tool is essential for assessing the safety of a medication for patients who are pregnant, planning to become pregnant, or breastfeeding. It provides the official guidance and available data on potential risks. Args: drug_name (str): Generic drug name preferred Returns: str: JSON string with pregnancy text, lactation text, and reproductive potential information """ result = drug_pregnancy_lactation(drug_name) return format_json_output(result) @with_error_handling @with_caching(ttl=7200) # 2 hours cache def drug_dose_adjustments_mcp(drug_name: str) -> str: """ Extracts dosing adjustment recommendations for patients with kidney (renal) or liver (hepatic) impairment from the official FDA drug label. This is a crucial tool for safe and effective dosing in special populations where standard doses may be harmful or ineffective. Args: drug_name (str): Generic drug name Returns: str: JSON string with renal and hepatic dosing excerpts """ result = drug_dose_adjustments(drug_name) return format_json_output(result) @with_error_handling @with_caching(ttl=1800) # 30 minutes cache def drug_livertox_summary_mcp(drug_name: str) -> str: """ Queries the NIH LiverTox database to retrieve a summary of a drug's potential for causing liver injury (hepatotoxicity). This tool is valuable for investigating or assessing the risk of drug-induced liver damage, providing information on the mechanism of injury, and clinical management advice. Args: drug_name (str): Drug name to search for (case-insensitive) Returns: str: JSON string with hepatotoxicity data, mechanism of injury, and management information """ result = drug_livertox_summary(drug_name) return format_json_output(result) @with_error_handling def brand_to_generic_lookup_mcp(brand_name: str) -> str: """ Converts a drug brand name to its generic (active ingredient) name. It can also provide information on the countries where the brand name is marketed. This tool is fundamental for identifying the active component of a branded medication, which is necessary for most other clinical lookups and to avoid therapeutic duplication. Args: brand_name (str): Brand name to look up Returns: str: JSON string with generic drug information and country-specific data """ result = _brand_lookup_gradio(brand_name) return format_json_output(result) @with_error_handling def calculate_drug_burden_index_mcp(drug_list: str) -> str: """ Calculates the Drug Burden Index (DBI) for a patient's medication list. The DBI is a measure of a person's total exposure to anticholinergic and sedative drugs, which are associated with an increased risk of falls and cognitive impairment, especially in the elderly. This intelligent version automatically detects the route of administration for each medication (e.g., oral, transdermal patches, parenteral injections) and uses the appropriate reference data for each route, making it suitable for complex, real-world medication regimens. Args: drug_list (str): Drug list (one per line, include dose and frequency - also write "prn" if the drug is a PRN medication) Examples: - "Fentanyl 25mcg/hr patch daily" - "Amitriptyline 25mg tablet twice daily" - "Morphine 10mg injection PRN" Returns: str: JSON string with DBI calculation results broken down by route and individual drug contributions """ result = _dbi_mcp_mixed_routes_gradio(drug_list) return format_json_output(result) @with_error_handling def calculate_creatinine_clearance_mcp( age: str, weight_kg: str, serum_creatinine: str, is_female: str ) -> str: """ Calculates a patient's creatinine clearance (CrCl) using the Cockcroft-Gault equation. CrCl is an estimate of the glomerular filtration rate (GFR) and is widely used to determine appropriate dose adjustments for drugs that are cleared by the kidneys. This is a standard clinical calculator for renal function assessment. Args: age (str): Patient's age in years weight_kg (str): Patient's weight in kilograms serum_creatinine (str): Patient's serum creatinine in mg/dL is_female (str): "true" if patient is female, "false" if male Returns: str: JSON string with creatinine clearance calculation and interpretation """ age_int = int(age) weight_float = float(weight_kg) creat_float = float(serum_creatinine) is_female_bool = is_female.lower() == "true" result = _cockcroft_gault_gradio( age_int, weight_float, creat_float, is_female_bool ) return format_json_output(result) @with_error_handling def calculate_egfr_mcp( age: str, serum_creatinine: str, is_female: str, is_black: str ) -> str: """ Calculates the estimated Glomerular Filtration Rate (eGFR) using the CKD-EPI 2021 equation. eGFR is a key indicator of kidney function and is used to diagnose, stage, and manage Chronic Kidney Disease (CKD). This is the preferred method for assessing kidney function in many clinical guidelines. Args: age (str): Patient's age in years serum_creatinine (str): Patient's serum creatinine in mg/dL is_female (str): "true" if patient is female, "false" if male is_black (str): "true" if patient is Black, "false" otherwise Returns: str: JSON string with eGFR calculation and CKD stage interpretation """ age_int = int(age) creat_float = float(serum_creatinine) is_female_bool = is_female.lower() == "true" is_black_bool = is_black.lower() == "true" result = _ckd_epi_gradio(age_int, creat_float, is_female_bool, is_black_bool) return format_json_output(result) @with_error_handling def calculate_child_pugh_score_mcp( bilirubin: str, albumin: str, inr: str, ascites: str, encephalopathy: str ) -> str: """ Calculates the Child-Pugh score, a well-established tool for assessing the prognosis of chronic liver disease, primarily cirrhosis. The score is used to determine the severity of liver disease and to guide dosage adjustments for drugs that are metabolized by the liver. Args: bilirubin (str): Total bilirubin in mg/dL albumin (str): Serum albumin in g/dL inr (str): INR value ascites (str): Ascites level ("none", "mild", "moderate-severe") encephalopathy (str): Encephalopathy grade ("none", "grade-1-2", "grade-3-4") Returns: str: JSON string with Child-Pugh score, class, and prognosis information """ bilirubin_float = float(bilirubin) albumin_float = float(albumin) inr_float = float(inr) result = _child_pugh_gradio( bilirubin_float, albumin_float, inr_float, ascites, encephalopathy ) return format_json_output(result) @with_error_handling def calculate_bmi_mcp(weight_kg: str, height_cm: str) -> str: """ Calculates a person's Body Mass Index (BMI) based on their weight and height. BMI is a widely used screening tool to categorize weight status (e.g., underweight, normal weight, overweight, obese) and identify potential health risks associated with weight. Args: weight_kg (str): Weight in kilograms height_cm (str): Height in centimeters Returns: str: JSON string with BMI calculation and weight category classification """ weight_float = float(weight_kg) height_float = float(height_cm) result = _bmi_gradio(weight_float, height_float) return format_json_output(result) @with_error_handling def calculate_ideal_body_weight_mcp(height_cm: str, is_male: str) -> str: """ Calculates a patient's Ideal Body Weight (IBW) using the Devine formula. IBW is used in various clinical contexts, including for calculating the dose of certain medications (e.g., aminophylline, digoxin) and for assessing nutritional status. Args: height_cm (str): Patient's height in cm is_male (str): "true" if patient is male, "false" if female Returns: str: JSON string with IBW calculation. """ height_float = float(height_cm) is_male_bool = is_male.lower() == "true" result = _ideal_body_weight_gradio(height_float, is_male_bool) return format_json_output(result) @with_error_handling def recommend_dosing_weight_mcp( actual_weight: str, height_cm: str, is_male: str ) -> str: """ Recommends the most appropriate weight to use for medication dosing calculations (i.e., actual, ideal, or adjusted body weight) based on the patient's actual weight and height. This is crucial for obese or underweight patients, as using the wrong weight can lead to sub-therapeutic or toxic drug levels for certain medications. Args: actual_weight (str): Patient's actual weight in kg height_cm (str): Patient's height in cm is_male (str): "true" if patient is male, "false" if female Returns: str: JSON string with dosing weight recommendation and rationale """ weight_float = float(actual_weight) height_float = float(height_cm) is_male_bool = is_male.lower() == "true" result = _dosing_weight_gradio(weight_float, height_float, is_male_bool) return format_json_output(result) @with_error_handling def convert_creatinine_units_mcp(value: str, from_unit: str, to_unit: str) -> str: """ Converts serum creatinine values between the two standard units of measurement: milligrams per deciliter (mg/dL) and micromoles per liter (μmol/L). This is essential for interoperability, as different laboratories and clinical calculators may use different units. Args: value (str): The creatinine value to convert from_unit (str): The original unit ("mg_dl" or "umol_l") to_unit (str): The target unit ("mg_dl" or "umol_l") Returns: str: JSON string with converted creatinine value and conversion factor """ value_float = float(value) result = _creatinine_conversion_gradio(value_float, from_unit, to_unit) return format_json_output(result) @with_error_handling def get_cache_stats_mcp() -> str: """ Retrieves statistics about the application's internal cache. This is a debugging and monitoring tool to assess the performance and health of the MCP server, showing metrics like hit rate and cache size. It is not typically used for clinical queries. Returns: str: JSON string with cache hit rates, size, and other metrics """ stats = api_cache.get_stats() expired_cleared = api_cache.clear_expired() result = { **stats, "expired_entries_cleared": expired_cleared, "cache_health": "good" if stats.get("hit_rate", 0) > 0.3 else "poor" } return format_json_output(standardize_response(result, "cache_stats")) @with_error_handling def health_check_mcp() -> str: """ Performs a health check on the MCP server to ensure its core components are operational. This tool is used for system monitoring to verify that the server is running, the cache is working, and basic calculations can be performed. It's not intended for clinical use. Returns: str: JSON string with server health information """ # Test basic functionality try: # Test cache cache_stats = api_cache.get_stats() # Test a simple calculation test_calc = cockcroft_gault_creatinine_clearance(65, 70, 1.2, False) calc_working = test_calc.get("creatinine_clearance_ml_min") is not None # Check if reference data is loaded from pathlib import Path ref_file_exists = Path("dbi_reference_by_route.csv").exists() health_status = { "status": "healthy", "timestamp": datetime.now().isoformat(), "uptime_info": { "python_version": sys.version.split()[0], "cache_working": cache_stats is not None, "calculations_working": calc_working, "reference_data_available": ref_file_exists }, "cache_stats": cache_stats, "version": "1.1.0" } # Determine overall health if not calc_working or not ref_file_exists: health_status["status"] = "degraded" except Exception as e: health_status = { "status": "unhealthy", "timestamp": datetime.now().isoformat(), "error": str(e), "version": "1.1.0" } return format_json_output(standardize_response(health_status, "health_check")) # ===== NEW ADR ANALYSIS ENDPOINTS ===== @with_error_handling def enhanced_faers_search_mcp( drug_name: str, adverse_event: str = "", age_range: str = "", gender: str = "", serious_only: str = "false", limit: str = "100" ) -> str: """ Performs an advanced search of the FDA Adverse Event Reporting System (FAERS) database with powerful filtering options. This tool is designed for in-depth pharmacovigilance analysis, allowing users to narrow down adverse event reports by patient age, gender, and the seriousness of the event. It's ideal for identifying trends and patterns in drug safety data. Use this tool particularly if the user asks about a specific adverse event or reaction. Args: drug_name (str): Drug name to search for adverse_event (str): Specific adverse event/reaction to filter by (optional) age_range (str): Age range filter like "18-65" or ">65" (optional) gender (str): Gender filter "1" (male) or "2" (female) (optional) serious_only (str): "true" to only return serious adverse events limit (str): Maximum number of results (default "100") Returns: str: JSON string with enhanced case data including demographics and outcomes """ limit_int = int(limit) if limit.isdigit() else 100 serious_bool = serious_only.lower() == "true" # Convert empty strings to None adverse_event = adverse_event if adverse_event.strip() else None age_range = age_range if age_range.strip() else None gender = gender if gender.strip() in ["1", "2"] else None result = enhanced_faers_search( drug_name=drug_name, adverse_event=adverse_event, age_range=age_range, gender=gender, serious_only=serious_bool, limit=limit_int ) return format_json_output(standardize_response(result, "enhanced_faers_search")) @with_error_handling def calculate_naranjo_score_mcp( adverse_reaction_after_drug: str, reaction_improved_after_stopping: str, reaction_reappeared_after_readministration: str, alternative_causes_exist: str, reaction_when_placebo_given: str, drug_detected_in_blood: str, reaction_worse_with_higher_dose: str, similar_reaction_to_drug_before: str, adverse_event_confirmed_objectively: str, reaction_appeared_after_suspected_drug_given: str ) -> str: """ Calculates the Naranjo score, a standardized and widely used causality assessment tool to determine the probability that an adverse event is related to a specific drug. The score helps clinicians and researchers classify the likelihood of an adverse drug reaction (ADR) as doubtful, possible, probable, or definite. Sometimes the user might not have provided all the information, so you will need to ask for the missing information (remember to give them the option to say "unknown" if they don't know the answer) Args: adverse_reaction_after_drug (str): "yes", "no", "unknown" reaction_improved_after_stopping (str): "yes", "no", "unknown" reaction_reappeared_after_readministration (str): "yes", "no", "unknown" alternative_causes_exist (str): "yes", "no", "unknown" reaction_when_placebo_given (str): "yes", "no", "unknown" drug_detected_in_blood (str): "yes", "no", "unknown" reaction_worse_with_higher_dose (str): "yes", "no", "unknown" similar_reaction_to_drug_before (str): "yes", "no", "unknown" adverse_event_confirmed_objectively (str): "yes", "no", "unknown" reaction_appeared_after_suspected_drug_given (str): "yes", "no", "unknown" Returns: str: JSON string with score, probability category, and detailed breakdown """ result = calculate_naranjo_score( adverse_reaction_after_drug=adverse_reaction_after_drug, reaction_improved_after_stopping=reaction_improved_after_stopping, reaction_reappeared_after_readministration=reaction_reappeared_after_readministration, alternative_causes_exist=alternative_causes_exist, reaction_when_placebo_given=reaction_when_placebo_given, drug_detected_in_blood=drug_detected_in_blood, reaction_worse_with_higher_dose=reaction_worse_with_higher_dose, similar_reaction_to_drug_before=similar_reaction_to_drug_before, adverse_event_confirmed_objectively=adverse_event_confirmed_objectively, reaction_appeared_after_suspected_drug_given=reaction_appeared_after_suspected_drug_given ) return format_json_output(standardize_response(result, "naranjo_score")) @with_error_handling def disproportionality_analysis_mcp( drug_name: str, adverse_event: str, background_limit: str = "10000" ) -> str: """ Performs a disproportionality analysis (also known as signal detection) on adverse event data. This statistical method compares the reporting rate of a specific drug-event combination to the reporting rate of that event for all other drugs in the database. It calculates metrics like Proportional Reporting Ratio (PRR) and Reporting Odds Ratio (ROR) to identify potential safety signals that may warrant further investigation. Args: drug_name (str): Drug of interest adverse_event (str): Adverse event of interest background_limit (str): Number of background cases to sample (default "10000") Returns: str: JSON string with PRR, ROR, IC values and statistical significance """ background_limit_int = int(background_limit) if background_limit.isdigit() else 10000 result = disproportionality_analysis( drug_name=drug_name, adverse_event=adverse_event, background_limit=background_limit_int ) return format_json_output(standardize_response(result, "disproportionality_analysis")) @with_error_handling def find_similar_cases_mcp( reference_case_id: str, similarity_threshold: str = "0.7", limit: str = "50" ) -> str: """ Identifies and retrieves adverse event cases from the FAERS database that are similar to a given reference case. Similarity is calculated based on a combination of patient demographics (age, gender), reported reactions, and concomitant drugs. This tool is useful for contextualizing a specific case and identifying potential case series for further review. Args: reference_case_id (str): FAERS safety report ID to use as reference similarity_threshold (str): Minimum similarity score 0-1 (default "0.7") limit (str): Maximum number of similar cases to return (default "50") Returns: str: JSON string with similar cases and similarity scores """ try: similarity_threshold_float = float(similarity_threshold) except ValueError: similarity_threshold_float = 0.7 limit_int = int(limit) if limit.isdigit() else 50 result = find_similar_cases( reference_case_id=reference_case_id, similarity_threshold=similarity_threshold_float, limit=limit_int ) return format_json_output(standardize_response(result, "similar_cases")) @with_error_handling def temporal_analysis_mcp( drug_name: str, adverse_event: str = "", limit: str = "500" ) -> str: """ Analyzes the temporal relationship between drug administration and the onset of adverse events. This tool provides insights into the typical time-to-onset for a specific drug-associated adverse event, which can be a critical factor in causality assessment. It helps determine if the timing of the event is consistent with the drug's known pharmacology. Args: drug_name (str): Drug to analyze adverse_event (str): Specific adverse event (optional) limit (str): Maximum cases to analyze (default "500") Returns: str: JSON string with temporal patterns and time-to-onset analysis """ adverse_event = adverse_event if adverse_event.strip() else None limit_int = int(limit) if limit.isdigit() else 500 result = temporal_analysis( drug_name=drug_name, adverse_event=adverse_event, limit=limit_int ) return format_json_output(standardize_response(result, "temporal_analysis")) with gr.Blocks( theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"), title="Pharmacist MCP", analytics_enabled=False ) as demo: gr.Markdown( """
A suite of tools for pharmacists and clinicians.
Access critical drug information, perform clinical calculations, and analyze adverse drug reaction data efficiently.