Spaces:
Runtime error
Runtime error
| from dotenv import load_dotenv | |
| from langchain_core.tools import tool | |
| import math | |
| from datetime import datetime, timedelta | |
| from langchain_core.tools import tool | |
| import matplotlib.pyplot as plt | |
| from langchain.agents import initialize_agent, AgentType | |
| from langchain_core.tools import tool | |
| from datetime import datetime | |
| from typing import Dict, Union | |
| from langchain_google_genai import ChatGoogleGenerativeAI | |
| import math | |
| import random | |
| import os | |
| import gradio as gr | |
| #load | |
| load_dotenv() | |
| api_key = os.getenv("GEMINI_API_KEY") | |
| # Line 23 should have ZERO spaces at the start if it's not in a function | |
| llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", api_key=api_key) | |
| CORRECT_PASSWORD =os.getenv("passCS") | |
| # Dictionary of dose limits (units in mSv) | |
| OCCUPATIONAL_LIMITS = { | |
| "Annual dose limit to Workers": 50, # mSv per year | |
| "Annual Exposure Limit to Workers (5-year avg)": 20, # mSv per year | |
| "Monthly dose Limit to Worker": 4.16, # mSv per month (approx.) | |
| "Lens of Eye Dose Limit to Worker": 50, # mSv per year | |
| "Extremity Dose Limit to Worker": 500, # mSv per year | |
| "Annual Dose Limit to Public": 1, # mSv per year | |
| "Lens of Eye Dose Limit to Public": 15, # mSv per year | |
| "Extremity Dose Limit to Public": 50, # mSv per year | |
| "Annual Dose Limit to Caregivers": 5, # mSv per year | |
| "Annual Dose Limit to Comforter": 5, # mSv per year | |
| "PNRA release limit(dose)": 50, # USv | |
| "PNRA release limit(activity)": 30, # mci | |
| "Discharge TEDE Threshold":0.5, #rem | |
| "Occupancy factor (6/24)":0.25, #if patient expose 6hr public a day | |
| "Occupancy factor (3/24)":0.125, #if public expose 3hr a day from patient | |
| } | |
| # ✅ Dictionary of Radioactive Nuclide & Their Half-Lives (in Days) | |
| from datetime import datetime | |
| import math | |
| RADIOACTIVE_NUCLIDES = { | |
| "Uranium-238": 4.47e9 * 365, # Billion years converted to days | |
| "Uranium-235": 704e6 * 365, # Million years converted to days | |
| "Thorium-232": 14.05e9 * 365, | |
| "Plutonium-239": 24100, | |
| "Plutonium-238": 87.7, | |
| "Radon-222": 3.8, | |
| "Radium-226": 1600, | |
| "Polonium-210": 138, | |
| "Americium-241": 432, | |
| "Carbon-14": 5730, | |
| "Strontium-90": 28.8 * 365, | |
| "Iodine-131 (I-131)": 8.02, | |
| "I-131": 8.02, | |
| "Iodine-125(I-125)": 59.4, | |
| "I-125": 59.4, | |
| "Cesium-137": 30.2 * 365, | |
| "Cs-137": 30.2 * 365, | |
| "Technetium-99m": 0.25, # 6 hours converted to days | |
| "Tc-99m": 0.25, # 6 hours converted to days | |
| "Cobalt-60": 5.27 * 365, | |
| "Co-60": 5.27 * 365, | |
| "Cobalt-57": 271.8, | |
| "Co-57": 271.8, | |
| "Tritium (H-3)": 12.32 * 365, | |
| "Krypton-85": 10.76 * 365, | |
| "Ruthenium-106": 373.6, | |
| "Molybdenum (MO-99)": 66/24, | |
| "Cesium Cs-137": 10950, # ~30 years | |
| "Cobalt Co-60": 1925, # ~5.3 years | |
| "Cobalt Co-57": 271.8, # ~9 months | |
| "Barium Ba-133": 3843, # ~10.5 years | |
| } | |
| # Half-life values (in days) | |
| HALF_LIFE = { | |
| "Cs-137": 10950, # ~30 years | |
| "Co-60": 1925, # ~5.3 years | |
| "Co-57": 271.8, # ~9 months | |
| "Ba-133": 3843, # ~10.5 years | |
| } | |
| # Source purchase dates (YYYY-MM-DD) | |
| SOURCE_PURCHASE = { | |
| "Cs-137": "2019-09-20", | |
| "Co-60": "2019-09-20", | |
| "Co-57": "2022-04-21", | |
| "Ba-133": "2019-09-20", | |
| } | |
| # Initial activities (in MBq) at the time of purchase (Example Values) | |
| INITIAL_ACTIVITY = { | |
| "Cs-137": 370, # Example values, replace with actual | |
| "Co-60": 1000, | |
| "Co-57": 740, | |
| "Ba-133": 500, | |
| } | |
| # Tool 1: Unit Conversion | |
| def unit_conversion_tool(value, from_unit, to_unit): | |
| """ | |
| Converts radiation measurement units. | |
| """ | |
| conversion_factors = { | |
| "Gy_to_rad": 100, "rad_to_Gy": 0.01, | |
| "Sv_to_rem": 100, "rem_to_Sv": 0.01, | |
| "Bq_to_Ci": 2.7e-11, "Ci_to_Bq": 3.7e10, | |
| "C/kg_to_R": 2.58e-4, "R_to_C/kg": 0.387e4, | |
| } | |
| key = f"{from_unit}_to_{to_unit}" | |
| if key in conversion_factors: | |
| result = value * conversion_factors[key] | |
| return f"**Conversion:** {value} {from_unit} → {to_unit}\n**Result:** {result:.4f} {to_unit}" | |
| else: | |
| return "⚠️ Invalid conversion request." | |
| # Tool 2: Radioactive Decay Calculation | |
| # Function to Calculate Radioactive Decay | |
| def radioactive_decay_tool(nuclide, initial_activity, elapsed_days): | |
| """ | |
| Computes remaining radioactive activity based on the nuclide, initial activity, and elapsed time. | |
| Generates a Medical Physics (MP) report in Google Doc style. | |
| Args: | |
| nuclide (str): Name of the radioactive nuclide. | |
| initial_activity (float): Initial activity in Bq or Ci or mCi. | |
| elapsed_days (float): Time elapsed in days . | |
| Returns: | |
| str: A formatted Google Doc-style report with computed decay results. | |
| """ | |
| initial_activity = float(initial_activity) | |
| elapsed_days = int(elapsed_days) | |
| # Now comparisons will work correctly | |
| if initial_activity <= 0 or elapsed_days < 0: | |
| return "Error: Initial activity must be positive, and elapsed days cannot be negative." | |
| # Retrieve half-life from the dictionary | |
| half_life = RADIOACTIVE_NUCLIDES.get(nuclide, None) | |
| H_L=HALF_LIFE.get(nuclide,None) | |
| #if half_life is None: | |
| if H_L is None: | |
| return f"⚠️ Error: Nuclide '{nuclide}' not found in database. Please check spelling or add it." | |
| # Compute decay constant | |
| #decay_constant = math.log(2) / half_life | |
| decay_constant = math.log(2) / H_L | |
| # Compute remaining activity | |
| remaining_activity = initial_activity * math.exp(-decay_constant * elapsed_days) | |
| # Generate Google Doc-style Report | |
| current_date = datetime.now().strftime("%Y-%m-%d") | |
| report = f""" | |
| # 📊 **Medical Physics Report** | |
| ### 📅 Date: {current_date} | |
| --- | |
| ## **🔹 Radioactive Decay Calculation** | |
| 🔬 **Radionuclide:** {nuclide} | |
| 📈 **Initial Activity:** {initial_activity} Bq or Ci or mCi | |
| ⏳ | |
| **HalfLife:**{H_L} | |
| 🕰️ **Elapsed Time:** {elapsed_days} days | |
| ⚛️ **Remaining Activity:** {remaining_activity:.4f} Bq or Ci or mCi | |
| --- | |
| 🔚 **End of Report** | |
| """ | |
| #**Half-life:** {half_life:.2f} days | |
| #return report | |
| # Tool 3: print list of exposure limits | |
| def print_exposure_limits(query: str = "all") -> str: | |
| """ | |
| Returns a SNIF Google Doc–style report of exposure/dose limits for workers, public, or caregivers. | |
| Args: | |
| query (str): A string indicating which limits to display. Acceptable values:"workers", "public", "caregivers","comforter" or "all" (default prints all limits). | |
| Returns: | |
| str: A formatted report with the requested dose limits. | |
| """ | |
| query_lower = query.lower().strip() | |
| # Filter keys based on the query | |
| if query_lower == "workers": | |
| # Include keys related to workers (e.g., any key with "worker") | |
| keys = [k for k in OCCUPATIONAL_LIMITS if "worker" in k.lower()] | |
| elif query_lower == "public": | |
| keys = [k for k in OCCUPATIONAL_LIMITS if "public" in k.lower()] | |
| elif query_lower == "caregivers": | |
| keys = [k for k in OCCUPATIONAL_LIMITS if "caregiver" in k.lower()] | |
| elif query_lower == "comforter": | |
| keys = [k for k in OCCUPATIONAL_LIMITS if "comforter" in k.lower()] | |
| else: | |
| # If query is "all" or unrecognized, print all limits. | |
| keys = list(OCCUPATIONAL_LIMITS.keys()) | |
| # Generate a SNIF Google Doc-style report | |
| current_date = datetime.now().strftime("%Y-%m-%d") | |
| report = f""" | |
| # **Exposure/Dose Limits Report** | |
| ### 📅 Date: {current_date} | |
| --- | |
| """ | |
| for key in keys: | |
| report += f"\n- **{key}**: {OCCUPATIONAL_LIMITS[key]} mSv\n" | |
| report += "\n---\n🔚 **End of Report**" | |
| return report | |
| # Tool 4:patient release criteria | |
| def patient_release_decision(neck_dose_microSv_hr: float, exposure_duration_hr: float, initial_activity_mCi: float, sef: str) -> str: | |
| """ | |
| Evaluates whether a patient treated with I-131 can be released based on their neck dose, | |
| remaining activity, and Socio-Economic Factor (SEF). Generates a structured SNIF Google Doc-style report. | |
| Args: | |
| neck_dose_microSv_hr (float): Measured neck dose in μSv/hr. | |
| exposure_duration_hr (float): Elapsed time in hours since administration. | |
| initial_activity_mCi (float): Administered activity in mCi. | |
| sef (str): Socio-Economic Factor, which should be either "good" or "bad". | |
| Returns: | |
| str: A formatted SNIF Google Doc-style report with the calculated TEDE and release recommendation. | |
| """ | |
| # Validate input | |
| if neck_dose_microSv_hr < 0 or exposure_duration_hr < 0 or initial_activity_mCi <= 0: | |
| return "⚠️ Invalid input! Neck dose, exposure duration, and initial activity must be positive numbers." | |
| sef = sef.lower().strip() | |
| if sef not in ["good", "bad"]: | |
| return "⚠️ Invalid SEF! Please provide 'GOOD' or 'BAD' for the socio-economic factor." | |
| # Retrieve Constants | |
| occupancy_factor = OCCUPATIONAL_LIMITS.get("Occupancy factor (6/24)", 0.25) | |
| discharge_threshold = OCCUPATIONAL_LIMITS.get("Discharge TEDE Threshold", 0.5) | |
| pnra_limit = OCCUPATIONAL_LIMITS.get("PNRA release limit(dose)", 50) # Default 50 µSv | |
| pnra_limit1= OCCUPATIONAL_LIMITS.get("PNRA release limit(activity)", 30)#default 30 mci | |
| half_life_days = RADIOACTIVE_NUCLIDES.get("I-131", 8.02) # Half-life in days | |
| # Convert exposure duration to days | |
| exposure_duration_days = exposure_duration_hr / 24 | |
| # Calculate Decay Constant (λ) in per day | |
| decay_constant = math.log(2) / half_life_days # λ = ln(2) / T_half | |
| # Compute Remaining Activity After Decay | |
| remaining_activity_mCi = initial_activity_mCi * math.exp(-decay_constant * exposure_duration_days) | |
| # Compute TEDE (Dose-Based) from Measured Neck Dose | |
| tede_dose_rem = (neck_dose_microSv_hr * 1.44 * 24 * half_life_days * occupancy_factor) / 1000 * 0.1 # Convert to rem | |
| # Compute TEDE (Activity-Based) from Remaining I-131 Activity | |
| tede_activity_rem = (remaining_activity_mCi * 1.44 * 24 * 2.2 * half_life_days * occupancy_factor) / 1000 * 0.1 # Convert to rem | |
| # Debugging Print Statements | |
| print(f"Remaining Activity (mCi): {remaining_activity_mCi:.3f}") | |
| print(f"TEDE from Dose Measurement (rem): {tede_dose_rem:.3f}") | |
| print(f"TEDE from Activity Calculation (rem): {tede_activity_rem:.3f}") | |
| print(f"PNRA release Limit: on Measured Dose{pnra_limit} µSv") | |
| print(f"PNRA release Limit: on Residual Activity{pnra_limit1} mCi") | |
| # **Decision-Making Based on NRC Guidelines** | |
| if tede_dose_rem < discharge_threshold and tede_activity_rem < discharge_threshold: | |
| recommendation = "✅ Immediate release recommended." | |
| elif tede_dose_rem < discharge_threshold and tede_activity_rem > discharge_threshold: | |
| if sef == "good": | |
| recommendation = "⚠️ Discharge allowed with strict home isolation guidelines." | |
| else: | |
| recommendation = "❌ Hospital stay recommended until TEDE (activity) < 0.5 rem." | |
| else: | |
| recommendation = "❌ Patient should remain hospitalized until both TEDE values are below 0.5 rem." | |
| # Generate SNIF Google Doc-style Report | |
| current_date = datetime.now().strftime("%Y-%m-%d") | |
| report = f""" | |
| # 📊 **Medical Physics Patient Release Report** | |
| ### 📅 Date: {current_date} | |
| --- | |
| ## **🔹 Patient Exposure Assessment** | |
| - **Neck Dose Rate:** {neck_dose_microSv_hr:.2f} μSv/hr | |
| - **Total Effective Dose Equivalent (TEDE from Measured Dose):** {tede_dose_rem:.3f} rem | |
| - **Total Effective Dose Equivalent (TEDE from Activity Calculation):** {tede_activity_rem:.3f} rem | |
| - **PNRA Release Limit:** on Measured dose {pnra_limit} µSv and on Residual Activity {pnra_limit1}mCi | |
| - **Remaining Activity After {exposure_duration_days:.2f} Days:** {remaining_activity_mCi:.3f} mCi | |
| ## **🔸 Definitions** | |
| - **TEDE (Activity-Based):** Estimated radiation exposure based on the remaining I-131 activity in the body which contribute to exposure to public. | |
| - **TEDE (Dose-Based):** Radiation exposure measured from the patient's emitted dose using a survey meter. | |
| ## **🔹 Discharge Recommendation** | |
| - **Threshold for Release:** {discharge_threshold} rem | |
| - **Occupancy Factor:** {occupancy_factor} | |
| - **Half-Life (I-131):** {half_life_days} days | |
| - **Decision:** {recommendation} | |
| --- | |
| 🔚 **End of Report** | |
| """ | |
| return report | |
| # ✅ Example Function Call | |
| #print(patient_release_decision(12.5, 48, 30, "good")) # Example with 30 mCi initial dose, 48 hours exposure | |
| # Tool-5:AI Agent to Call Tools | |
| def medical_physics_agent(query): | |
| """ | |
| An AI agent that takes user queries and calls the appropriate MP tool. | |
| """ | |
| current_date = datetime.now().strftime("%Y-%m-%d") | |
| # Identify Query Type | |
| if "convert" in query: | |
| value = float(query.split()[1]) # Extract first number | |
| from_unit, to_unit = query.split()[2], query.split()[4] # Extract units | |
| result = unit_conversion_tool(value, from_unit, to_unit) | |
| title = "Unit Conversion" | |
| elif "decay" in query: | |
| values = [float(i) for i in query.split() if i.replace('.', '', 1).isdigit()] | |
| if len(values) == 3: | |
| result = radioactive_decay_tool(values[0], values[1], values[2]) | |
| title = "Radioactive Decay Calculation" | |
| else: | |
| return "⚠️ Invalid input format for decay calculation." | |
| elif "dose" in query: | |
| dose = float(query.split()[1]) | |
| result = radiation_protection_advice(dose) | |
| title = "Occupational Dose Assessment" | |
| elif "predicted yield" in query: | |
| activity = float(query.split()[1]) | |
| result = predict_tc99m_yield(datetime,activity) | |
| title = "Predicted Yield of Tc-99m" | |
| elif "nm_test_protocol" in query: | |
| nm_test = float(query.split()[1]) | |
| result = nm_test_protocol(nmTest,weight,height) | |
| title = "nm_test_protocol" | |
| elif "QC Tests DOSE CALIBRATOR" in query: | |
| # Example Usage | |
| QC_test = float(query.split()[1]) | |
| result = get_qc_test_details (QC_test) | |
| title = "QC_test_protocol" | |
| elif "QC Tests GAMMA CAMERA" in query: | |
| # Example Usage | |
| QC_test = float(query.split()[1]) | |
| result = get_gammaqc_test_details (QC_test) | |
| title = "QC_test_protocol" | |
| else: | |
| return "⚠️ Sorry, I didn't understand your request." | |
| # Format Response in Google Doc SNIF Style | |
| report = f""" | |
| # **📊 Medical Physics (MP) Report** | |
| ### **📅 Date:** {current_date} | |
| --- | |
| ## **{title}** | |
| {result} | |
| --- | |
| **End of Report** | |
| """ | |
| return report | |
| #Tool-6 RP-Advice | |
| def radiation_protection_advice(daily_exposure_microSv): | |
| """ | |
| Function to calculate monthly radiation exposure and provide safety advice | |
| based on regulatory limits. | |
| Parameters: | |
| daily_exposure_microSv (float): Radiation exposure in microSievert (µSv) for an 8-hour workday. | |
| Returns: | |
| str: Radiation safety advice. | |
| """ | |
| # Constants | |
| WORK_DAYS_PER_MONTH = 22 # Assuming 22 working days in a month | |
| MONTHLY_LIMIT_mSv = OCCUPATIONAL_LIMITS["Monthly dose Limit to Worker"] # Monthly regulatory limit (20 mSv/year → 1.67 mSv/month) | |
| # Convert daily exposure from microSv to mSv | |
| daily_exposure_mSv = daily_exposure_microSv / 1000 # 1 mSv = 1000 µSv | |
| # Calculate Monthly Exposure | |
| monthly_exposure_mSv = daily_exposure_mSv * WORK_DAYS_PER_MONTH | |
| # Provide safety advice based on exposure levels | |
| advice = f"Estimated Monthly Dose: {monthly_exposure_mSv:.3f} mSv\n" | |
| if monthly_exposure_mSv < MONTHLY_LIMIT_mSv: | |
| advice += "✅ Exposure is within the safe limits. Continue monitoring and follow ALARA principles." | |
| elif monthly_exposure_mSv < 2 * MONTHLY_LIMIT_mSv: | |
| advice += "⚠️ Exposure is approaching the limit. Reduce unnecessary exposure, optimize shielding, and monitor closely." | |
| else: | |
| advice += "🚨 Exposure exceeds safe limits! Immediate review required. Restrict exposure and implement strict radiation safety measures." | |
| return advice | |
| # Tool7-Predicted Yield Tc-99m | |
| def predict_tc99m_yield(arrival_date, initial_activity): | |
| """ | |
| Predicts the daily yield of a Tc-99m generator for the next 7 days. | |
| Parameters: | |
| arrival_date (str): Date when the generator arrives (format: "DD-MM-YYYY"). | |
| initial_activity (float): Initial activity of Mo-99 in mCi. | |
| Returns: | |
| dict: Dictionary with dates as keys and predicted yields as values. | |
| """ | |
| # Constants | |
| #half_life_Mo99 = 66 # Mo-99 half-life in hours | |
| half_life_Mo99=RADIOACTIVE_NUCLIDES["Molybdenum (MO-99)"]*24 | |
| #print(f"half life ofMo-99{half_life_Mo99}") | |
| decay_constant = math.log(2) / half_life_Mo99 # Decay constant | |
| extraction_efficiency = 0.87 # Typical Tc-99m extraction efficiency | |
| # Convert string date to datetime object | |
| arrival_datetime = datetime.strptime(arrival_date, "%d-%m-%Y") | |
| # Predict yield for the next 7 days | |
| predicted_yields = {} | |
| for day in range(1, 8): # Next 7 days | |
| current_date = arrival_datetime + timedelta(days=day) | |
| time_elapsed = day * 24 # Convert days to hours | |
| remaining_activity = initial_activity * math.exp(-decay_constant * time_elapsed) | |
| tc99m_yield = remaining_activity * extraction_efficiency # Apply efficiency | |
| predicted_yields[current_date.strftime("%d-%m-%Y")] = round(tc99m_yield, 2) | |
| return predicted_yields | |
| ###TOOL8NM-SCAN PROTOCOL as per SNMMI | |
| def nm_test_protocol(test_name, weight=None, height=None, age=None): | |
| """ | |
| Function to return the nuclear medicine test protocol based on user query. | |
| Parameters: | |
| - test_name (str): Name of the NM test | |
| - weight (float): Patient weight in kg (for pediatric cases) | |
| - height (float): Patient height in cm (optional, not used for dosing) | |
| - age (int): Patient age in years (to determine adult vs pediatric dose) | |
| Returns: | |
| - A formatted string with test details. | |
| """ | |
| # Database of NM test protocols | |
| nm_tests = { | |
| "bone scan": { | |
| "radiopharmaceutical": "99mTc-MDP/HDP", | |
| "dose_adult": "10–25 mCi (370–925 MBq)", | |
| "dose_pediatric": lambda w: f"{round(min(0.3 * w, 25), 2)} mCi ({round(min(11.1 * w, 925), 2)} MBq)", | |
| "imaging_time": "2–4 hrs", | |
| "preparation": "Hydrate well", | |
| "max_ped_dose": "25 mCi" | |
| }, | |
| "mag3 renal scan": { | |
| "radiopharmaceutical": "99mTc-MAG3", | |
| "dose_adult": "3–10 mCi (111–370 MBq)", | |
| "dose_pediatric": lambda w: f"{round(min(0.1 * w, 10), 2)} mCi ({round(min(3.7 * w, 370), 2)} MBq)", | |
| "imaging_time": "Immediate", | |
| "preparation": "Hydrate well", | |
| "max_ped_dose": "10 mCi" | |
| }, | |
| "dtpa renal scan": { | |
| "radiopharmaceutical": "99mTc-DTPA", | |
| "dose_adult": "3–10 mCi (111–370 MBq)", | |
| "dose_pediatric": lambda w: f"{round(min(0.2 * w, 10), 2)} mCi ({round(min(7.4 * w, 370), 2)} MBq)", | |
| "imaging_time": "Immediate", | |
| "preparation": "Hydrate well", | |
| "max_ped_dose": "10 mCi" | |
| }, | |
| "dmsa renal scan": { | |
| "radiopharmaceutical": "99mTc-DMSA", | |
| "dose_adult": "1–5 mCi (37–185 MBq)", | |
| "dose_pediatric": lambda w: f"{round(min(0.3 * w, 5), 2)} mCi ({round(min(11.1 * w, 185), 2)} MBq)", | |
| "imaging_time": "2–4 hrs", | |
| "preparation": "None", | |
| "max_ped_dose": "5 mCi" | |
| }, | |
| "hida scan": { | |
| "radiopharmaceutical": "99mTc-DISIDA/MeBrofenin", | |
| "dose_adult": "3–8 mCi (111–296 MBq)", | |
| "dose_pediatric": lambda w: f"{round(min(0.1 * w, 8), 2)} mCi ({round(min(3.7 * w, 296), 2)} MBq)", | |
| "imaging_time": "Immediate", | |
| "preparation": "NPO 4–6 hrs", | |
| "max_ped_dose": "8 mCi" | |
| }, | |
| "cardiac MIBI scan": { | |
| "radiopharmaceutical": "99mTc-MIBI/Tetrofosmin", | |
| "dose_adult": "8–36 mCi (296–1332 MBq)", | |
| "dose_pediatric": lambda w: f"{round(min(0.3 * w, 36), 2)} mCi ({round(min(11.1 * w, 1332), 2)} MBq)", | |
| "imaging_time": "15–60 min", | |
| "preparation": "NPO 4–6 hrs, Avoid caffeine", | |
| "max_ped_dose": "36 mCi" | |
| } | |
| } | |
| # Normalize the test name to lowercase for matching | |
| test_name = test_name.lower() | |
| # Check if the test exists | |
| if test_name not in nm_tests: | |
| return "❌ Test not found. Please enter a valid NM test name." | |
| # Get test details | |
| test_details = nm_tests[test_name] | |
| # Determine dose based on age | |
| if age is None or age >= 18: | |
| dose_info = f"**Dose (Adult):** {test_details['dose_adult']}" | |
| elif weight is not None: | |
| dose_info = f"**Dose (Pediatric - Weight {weight} kg):** {test_details['dose_pediatric'](weight)} (Max: {test_details['max_ped_dose']})" | |
| else: | |
| dose_info = "**Pediatric dose requires weight input.**" | |
| # Format output | |
| result = f""" | |
| 🔬**Nuclear Medicine Test Protocol at AEMCK: {test_name.title()}** | |
| ______As per SNMMI | |
| - **Radiopharmaceutical:** {test_details['radiopharmaceutical']} | |
| {dose_info} | |
| - **Imaging Time:** {test_details['imaging_time']} | |
| - **Patient Preparation:** {test_details['preparation']} | |
| """ | |
| return result.strip() | |
| # Example Usage | |
| #print(nm_test_protocol("Bone Scan", weight=15, age=5)) | |
| #print(nm_test_protocol("Cardiac MIBI Scan", age=25)) | |
| #print(nm_test_protocol("HIDA Scan", weight=30, age=10)) | |
| #Curent activity Tool9 | |
| def calculate_current_activity(source_name): | |
| """ | |
| Calculates the current activity of a radioactive source based on the decay formula. | |
| Args: | |
| source_name (str): Name of the isotope (e.g., "Cs-137", "Co-60"). | |
| Returns: | |
| float: Current activity in MBq. | |
| """ | |
| if source_name not in HALF_LIFE or source_name not in SOURCE_PURCHASE: | |
| return "Unknown Source" | |
| # Get today's date and calculate decay | |
| today = datetime.today() | |
| purchase_date = datetime.strptime(SOURCE_PURCHASE[source_name], "%Y-%m-%d") | |
| days_elapsed = (today - purchase_date).days | |
| # Decay formula: A = A0 * (1/2)^(t/T) | |
| A0 = INITIAL_ACTIVITY[source_name] | |
| T = HALF_LIFE[source_name] | |
| A = A0 * math.pow(0.5, days_elapsed / T) | |
| return round(A, 2) # Return current activity in MBq | |
| #For QC-TESTTOOL#10 | |
| def get_qc_test_details(test_name): | |
| """ | |
| Retrieves QC test details of DoseCalibrator CRC-25R, including frequency, acceptance values, instructions, and good practices." | |
| Args: | |
| test_name (str): The name of the QC test (e.g., "Accuracy", "Constancy Test", "Peak Positioning", "Energy Resolution,"). | |
| Returns: | |
| dict: A dictionary containing test details or an error message if the test is invalid. | |
| Example Usage: | |
| >>> get_qc_test_details("Peak positioning") | |
| """ | |
| qc_tests = { | |
| "Introduction":["AEMCK (Atomic Energy Medical Centre Karachi) has one Dose Calibrator", | |
| "CRC-25R and three Gamma Cameras for Diagnosis and Therapies.", | |
| "Daily QC includes Peak Position for Gamma cam, Energy Resolution, Background Test, and Image Quality Test.", | |
| "Monthly QC includes Centre of Rotation (COR) and Intrinsic Uniformity Test."], | |
| "Chamber Voltage": { | |
| "Frequency": "Daily", | |
| "Acceptance Value": "±1%", | |
| "Instructions": [ | |
| "Turn on the dose calibrator and let it warm up.", | |
| "Check the voltage reading on the display.", | |
| "If deviation exceeds ±1%, recalibrate the system." | |
| ], | |
| "Good Practices": [ | |
| "Ensure a stable power supply.", | |
| "Perform at the same time daily for consistency." | |
| ] | |
| }, | |
| "Accuracy": { | |
| "Frequency": "Quarterly", | |
| "Acceptance Value": "±5%", | |
| "Instructions": [ | |
| "Select the isotope setting (Cs-137, Co-57, Co-60, Ba-133).", | |
| "Place the radioactive source in the dose calibrator.", | |
| "Compare the displayed activity with the expected activity.", | |
| "If deviation is greater than ±5%, recalibrate." | |
| ], | |
| "Good Practices": [ | |
| "Use standard sources: Co-57, Co-60, Ba-133, Cs-137.", | |
| "Perform test before patient doses to avoid delays." | |
| ], | |
| "Sources Used": { | |
| "Cs-137": calculate_current_activity("Cs-137"), | |
| "Co-60": calculate_current_activity("Co-60"), | |
| "Co-57": calculate_current_activity("Co-57"), | |
| "Ba-133": calculate_current_activity("Ba-133"), | |
| } | |
| }, | |
| "Constancy Test": { | |
| "Frequency": "Daily", | |
| "Acceptance Value": "±5% of reference value", | |
| "Instructions": [ | |
| "Use Cs-137 source for constancy checks.", | |
| "Measure the activity and compare with previous readings.", | |
| "Ensure the variation is within ±5%." | |
| ], | |
| "Good Practices": [ | |
| "Perform under consistent environmental conditions.", | |
| "If deviation is high, check for contamination or drift." | |
| ] | |
| }, | |
| "Zero Adjustment": { | |
| "Frequency": "Daily", | |
| "Acceptance Value": "±0.3 mV", | |
| "Good Practices": [ | |
| "Ensure no external radiation sources nearby.", | |
| "Check before starting daily calibration." | |
| ] | |
| }, | |
| "Background": { | |
| "Frequency": "Daily", | |
| "Acceptance Value": "±20% of current mean (<530 μCi)", | |
| "Good Practices": [ | |
| "Check for contamination in the measurement chamber.", | |
| "Ensure no residual radiation is affecting readings." | |
| ] | |
| }, | |
| "Data Check": { | |
| "Frequency": "Daily", | |
| "Acceptance Value": "Ensure correct readings", | |
| "Good Practices": [ | |
| "Verify readings against known reference sources.", | |
| "Report and recalibrate if discrepancies are found." | |
| ] | |
| }, | |
| "Contamination": { | |
| "Frequency": "Weekly", | |
| "Acceptance Value": "<3 μCi (0.12 MBq)", | |
| "Good Practices": [ | |
| "Use wipe tests to check for contamination.", | |
| "Regularly clean the dose calibrator to prevent false readings." | |
| ] | |
| }, | |
| "Linearity": { | |
| "Frequency": "Quarterly", | |
| "Acceptance Value": "5% of the expected value", | |
| "Good Practices": [ | |
| "Use multiple activity levels to check for linearity.", | |
| "Ensure readings remain consistent over different dose levels." | |
| ] | |
| }, | |
| "Geometry": { | |
| "Geometry QC Test": { | |
| "Frequency": "Acceptance Test (Once during installation or after major repairs or Annually)", | |
| "Acceptance Value": "Should remain within ±5% of expected value", | |
| "Instructions": [ | |
| "Ensure the dose calibrator is properly set up and warmed up.", | |
| "Select the appropriate isotope setting for the test (e.g., Tc-99m).", | |
| "Prepare a series of different volume samples like 3ml syringe and 20ml vial using the same activity concentration.", | |
| "Measure the activity of each sample in the dose calibrator.", | |
| "Compare the readings across different volumes to assess consistency.", | |
| "If variation exceeds ±5%, apply correction factors or recalibrate the system." | |
| ], | |
| "Good Practices": [ | |
| "Use a well-mixed radiopharmaceutical solution to maintain uniform activity concentration.", | |
| "Ensure consistent sample positioning inside the dose calibrator.", | |
| "Avoid air bubbles in syringes or vials as they can affect readings.", | |
| "Perform test with and without a syringe or vial to check geometry effects.", | |
| "Document results for future reference and consistency checks." | |
| ] | |
| }, | |
| } | |
| } | |
| #return qc_tests.get(test_name, {"Error": "Invalid QC Test Name"}) | |
| return qc_tests.get(test_name, {"Error": "Invalid test name. Please choose a valid QC test."}) | |
| #Tool 11 for gamma qc | |
| def get_gammaqc_test_details(test_name): | |
| """ | |
| Retrieves QC test details of Gamma Camera, including Peak positioning, Energy Resolution etc. | |
| Args: | |
| test_name (str): The name of the QC test (e.g, "Peak Positioning", "Energy Resolution,"). | |
| Returns: | |
| dict: A dictionary containing test details or an error message if the test is invalid. | |
| Example Usage: | |
| >>> get_qc_test_details("Peak positioning") | |
| """ | |
| qc_tests = { | |
| "Introduction":["AEMCK (Atomic Energy Medical Centre Karachi) has three Gamma Cameras for Diagnosis and Therapies.", | |
| "Daily QC includes Peak Position , Energy Resolution, Background Test, and Image Quality Test.", | |
| "Monthly QC includes Centre of Rotation (COR) and Intrinsic Uniformity Test."], | |
| "Peak Position": { | |
| "Frequency": "Daily", | |
| "Acceptance Value": "Tc-99m: 140 ± 3 keV, Co-57: 122 ± 3 keV", | |
| "Instructions": [ | |
| "Ensure proper calibration of the Gamma camera.", | |
| "Place the Tc-99m or Co-57 source appropriately.", | |
| "Verify that the peak position falls within the acceptable range." | |
| ], | |
| "Good Practices": [ | |
| "Always use a well-calibrated source for Gamma cam.", | |
| "Check for any sudden peak shifts indicating detector issues." | |
| ] | |
| }, | |
| "Energy Resolution": { | |
| "Frequency": "Daily", | |
| "Acceptance Value": "Tc-99m: < 11.0%, Co-57: < 12.0%", | |
| "Instructions": [ | |
| "Acquire a spectrum using the Gamma camera.", | |
| "Calculate the full-width at half maximum (FWHM).", | |
| "Ensure that the energy resolution falls within the acceptance criteria." | |
| ], | |
| "Good Practices": [ | |
| "Use a high-quality energy calibration source.", | |
| "Avoid fluctuations in environmental conditions." | |
| ] | |
| }, | |
| "Intrinsic Uniformity": { | |
| "Frequency": "Monthly", | |
| "Acceptance Value": "CFOV Integral Uniformity < 5%", | |
| "Instructions": [ | |
| "Perform an intrinsic uniformity scan using Tc-99m for Gamma Cam", | |
| "Analyze the image uniformity using processing software.", | |
| "Ensure that uniformity deviations remain within the threshold." | |
| ], | |
| "Good Practices": [ | |
| "Use a uniform flood source.", | |
| "Regularly check for detector malfunctions." | |
| ] | |
| }, | |
| "Centre of Rotation (COR)": { | |
| "Frequency": "Monthly", | |
| "Instructions": [ | |
| "Use a point source of Tc-99m at the center of rotation for Gamma cam.", | |
| "Acquire multiple views and analyze COR deviation.", | |
| "Ensure that misalignment does not exceed acceptable limits." | |
| ], | |
| "Good Practices": [ | |
| "Perform COR analysis after maintenance or detector adjustments.", | |
| "Verify consistency over multiple acquisitions." | |
| ] | |
| } | |
| } | |
| #return qc_tests.get(test_name, {"Error": "Invalid QC Test Name"}) | |
| return qc_tests.get(test_name, {"Error": "Invalid test name. Please choose a valid QC test."}) | |
| # Example Usage | |
| #if __name__ == "__main__": | |
| # test_name = input("Enter the QC test name: ") | |
| # result = get_qc_test_details(test_name) | |
| # # print(result) | |
| def random_prompt(): | |
| return random.choice([ | |
| "Convert 1Gy into rem.", | |
| "Tell me the half life of Iodine I-131.", | |
| "A patient with 50mci administered on 27-2-2025 after 1 day he has neckdose 25USv with good SEF, can I release patient?.", | |
| "List me all radiation dose limits for workers.", | |
| "Tell me the radiation dose limits for General Public as per PNRA.", | |
| "What is the predicted yield for next seven days of 600mci of Tc-99m generator which received on 10-2-2025.", | |
| "A cobalt-57 (Co-57) source of 10mCi,what will be the remaining activity after 100days?", | |
| "If a worker got 2mSv daily in working hours, what will be the advice of RPO in ALARA context.", | |
| "What is the dose protocol of HIDA scan for Pedriatic patient of 7kg weight and 3 years age.", | |
| "Tell me the good practices of Accuracy Qc test of Dose Callibrator.", | |
| "Geometry Qc test for Dose Callibrator instructions", | |
| ]) | |
| tools=[get_gammaqc_test_details,get_qc_test_details,calculate_current_activity,nm_test_protocol,radiation_protection_advice,predict_tc99m_yield,print_exposure_limits,patient_release_decision,unit_conversion_tool,radioactive_decay_tool] | |
| #agent=initialize_agent(tools,llm,agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION) | |
| agent = initialize_agent( | |
| tools=tools, | |
| llm=llm, | |
| agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION, | |
| verbose=True, | |
| max_iterations=3, # Limit steps to prevent slowness(addnew) | |
| handle_parsing_errors=True, | |
| early_stopping_method="generate" | |
| ) | |
| #response=agent.invoke({"input":" Neck dose of iodine treated patient is 20 microSv he is staying 24 hrs and his sef is BAD. "}) | |
| #print(f"\n{response}\n") | |
| # Define the function that handles user input | |
| def process_input(user_input): | |
| # Assuming agent.invoke(user_input) is your processing logic.You should replace this with the actual call to your agentFor example: response = agent.invoke(user_input) | |
| # For now, we are just simulating the response with a simple message | |
| response = agent.invoke(user_input) | |
| return response | |
| # Define the correct password | |
| #CORRECT_PASSWORD =os.getenv("app_password") | |
| #print(f"SECRET_KEY: {CORRECT_PASSWORD}") | |
| # Function to check password and return output | |
| def authenticate(password): | |
| if password == CORRECT_PASSWORD: | |
| success_message =" ✅ **Access Granted!" | |
| return success_message, gr.Textbox(visible=True), gr.Button(visible=True) | |
| else: | |
| failure_message = " ❌ **Access Denied!Incorrect Password" | |
| return failure_message, gr.Textbox(visible=False), gr.Button(visible=False) | |
| custom_css = """ | |
| <style> | |
| .custom-input { | |
| background-color: black; | |
| color: white; | |
| font-size: 16px; | |
| border-radius: 10px; | |
| padding: 10px; | |
| } | |
| .custom-output { | |
| background-color: #222; | |
| color: cyan; | |
| font-size: 16px; | |
| border-radius: 10px; | |
| padding: 10px; | |
| alignment(center); | |
| } | |
| .custom-button { | |
| background-color: blue; | |
| color: white; | |
| font-size: 18px; | |
| border-radius: 8px; | |
| padding: 10px 20px; | |
| font-weight: bold; | |
| } | |
| .custom-button:hover { | |
| background-color: darkblue; | |
| } | |
| .custom-json { | |
| font-size: 18px !important; /* Increase JSON output text size */ | |
| } | |
| </style> | |
| """ | |
| # Gradio UI | |
| with gr.Blocks() as ui: | |
| gr.Markdown(custom_css) | |
| gr.HTML(""" | |
| <style> | |
| button.svelte-1ipelgc { | |
| display: none !important; | |
| } | |
| </style> | |
| """) | |
| gr.HTML(""" | |
| <style> | |
| .custom-title { | |
| text-align: center; | |
| color: blue; | |
| font-size: 32px; | |
| font-weight: bold; | |
| } | |
| .custom-sub { | |
| text-align: center; | |
| color: gray; | |
| font-size: 16px; | |
| font-weight: bold; | |
| } | |
| .custom-page { | |
| text-align: center; | |
| color: green; | |
| font-size: 12px; | |
| } | |
| .custom-footer { | |
| text-align: right; | |
| color: orange; | |
| font-size: 12px; | |
| } | |
| </style> | |
| <h1 class="custom-title"></h1> | |
| <h2 class="custom-sub"></h2> | |
| """) | |
| #<p class="custom-page">An intelligent system for Radiation unit conversions, Activity and decay calculations, the management of Iodine-treated patient releases, Predicted yield of Tc-99m,ALARA advice to exposed worker, also the St.INJECTION PROTOCOLS for NM Scans, Ensures compliance with radiation protection regulations and QC standards for Modalities, integrating all relevant safety limits and guidelines with Security 🔒</p> | |
| # Inject inline CSS using Markdown (works properly in Gradio) | |
| gr.HTML(""" | |
| <div style="text-align: center;"> | |
| <span style="font-size: 24px; font-weight: bold; color: darkblue;">MEDICAL PHYSICS-NEXUS</span> | |
| <span style="font-size: 16px; color: gray;">(PART-I)</span><br> | |
| <span style="font-size: 16px; color: black;">Powered with Generative AI</span><br> | |
| <span style="font-size: 10px; color: green;"> | |
| An intelligent system for Radiation unit conversions, Activity and decay calculations, | |
| the management of Iodine-treated patient releases, Predicted yield of Tc-99m, | |
| ALARA advises exposed workers and the St. INJECTION PROTOCOLS for NM Scans. | |
| Ensures compliance with radiation protection regulations and QC standards for Modalities, | |
| integrating all relevant safety limits and guidelines with Security 🔒. | |
| </span> | |
| </div> | |
| """) | |
| # Display title and description | |
| #gr.Markdown("<h1 style='text-align:center ;margin-bottom:0.0px;margin-top:0.5px; color: blue;'>MedicalPhysics-Nexus</h1>",elem_classes="custom-output") | |
| #gr.Markdown("<h4 style='margin-top:0.0;margin-bottom:5px;text-align: center; color: Black;'>Powered with Generative AI</h4>") | |
| #gr.Markdown("<p style='text-align: center; color: green;'>An intelligent system for Radiation units conversions, Activity and decay calculations, the management of Iodine-treated patient releases, Predicted yield of Tc-99m,ALARA advice to exposed worker, also the St.INJECTION PROTOCOLS for NM Scans, Ensures compliance with radiation protection regulations and QC standards for Modalities, integrating all relevant safety limits and guidelines with Security 🔒.</p>") | |
| # Password Input | |
| # User input and output text | |
| password_input = gr.Textbox(label="Password Required", type="password", placeholder="Enter your password") | |
| submit_button1=gr.Button("authentication") | |
| auth_message = gr.Textbox(label="Status", interactive=False) | |
| # Initially hidden prompt input and submit button | |
| #prompt_input = gr.Textbox(label="Enter your prompt", visible=False) | |
| user_input = gr.Textbox(value=random_prompt,label="Enter your Prompt",lines=3,max_lines=5,elem_classes="custom-input") | |
| submit_button = gr.Button("Submit", visible=False) | |
| #response_output = gr.Textbox(label="AI Response", interactive=False) | |
| response_output=gr.JSON(label="formatted response",elem_classes="custom-json") | |
| #output_text = gr.JSON(label="Formatted Response\n") | |
| # Authenticate and reveal prompt field | |
| # password_input.submit(authenticate, inputs=password_input, outputs=[auth_message, user_input, submit_button]) | |
| submit_button1.click( | |
| authenticate, | |
| inputs=password_input, | |
| outputs=[auth_message,user_input,submit_button] | |
| ) | |
| # Process the prompt after authentication | |
| submit_button.click(process_input, inputs=user_input, outputs=response_output) | |
| # User input and output text | |
| #user_input = gr.Textbox(value=random_prompt,label="Enter your Prompt",lines=3,max_lines=5) | |
| #password_input = gr.Textbox(label="Password Required", type="password", placeholder="Enter your password") | |
| #output_text = gr.JSON(label="Formatted Response\n") | |
| #output_text = gr.Textbox(label="Response", interactive=False) | |
| # Submit button | |
| #submit_button = gr.Button("Submit") | |
| # Define button interaction | |
| #submit_button.click(fn=process_input, inputs=user_input, outputs=output_text) | |
| #submit_button.click(authenticate, inputs=password_input, outputs=output_text) | |
| # Footer text | |
| #gr.Markdown("<h3 style='text-align: right;font-size: 14.0px ; color: blue;'>Medical Physics Division,Atomic Energy Medical Centre Karachi.</h3>") | |
| # Footer using gr.HTML | |
| # gr.HTML(""" | |
| #<div style="text-align: right;"> | |
| #<span style="font-size:14px color: blue;"> 2025 Medical Physics Division, Atomic Energy Medical Centre.</span> | |
| #""") | |
| # </div> | |
| # """) | |
| gr.HTML(""" | |
| <footer style=" | |
| position: relative; | |
| bottom: 0; | |
| width: 100%; | |
| text-align: right; | |
| padding: 10px; | |
| background-color: #f8f9fa; | |
| color: #333; | |
| font-size: 12px; | |
| "> | |
| © 2025 Medical Physics Division, Atomic Energy Medical Centre. | |
| </footer> | |
| """) | |
| # Launch the Gradio app with public URL | |
| #ui.launch() | |
| #ui.launch(share=True, show_api=False) # Ensures no external sharing & API buttons | |
| ui.launch(show_api=False) | |
| #ui.launch(server_name="0.0.0.0", server_port=7860, show_api=False) | |
| #ui.launch(server_name="0.0.0.0", server_port=7860, show_api=False) |