Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env.production +29 -0
- .gitattributes +166 -0
- Dockerfile +79 -0
- README.es.md +129 -0
- README.md +3 -3
- agents/__init__.py +32 -0
- agents/corrective_rag.py +353 -0
- agents/critic.py +290 -0
- agents/formatter.py +182 -0
- agents/graph.py +260 -0
- agents/memory.py +162 -0
- agents/nodes.py +195 -0
- agents/router.py +161 -0
- agents/specialist.py +206 -0
- agents/state.py +105 -0
- agents/tools.py +365 -0
- app.py +146 -37
- config.json +1 -0
- data/clinical_guides/esmo/ESMO_PMC10416694_Developing_a_core_set_of_patient_reported_outcomes.pdf +3 -0
- data/clinical_guides/esmo/ESMO_PMC10664856_ESMO_ASCO_Recommendations_for_a_Global_Curriculum_.pdf +3 -0
- data/clinical_guides/esmo/ESMO_PMC10774906_ESMO_ASCO_Recommendations_for_a_Global_Curriculum_.pdf +3 -0
- data/clinical_guides/esmo/ESMO_PMC11574484_Effects_of_Baduanjin_exercise_on_cancer_related_fa.pdf +3 -0
- data/clinical_guides/esmo/ESMO_PMC11628549_Research_hotspots_and_trends_in_immunotherapy_for_.pdf +3 -0
- data/clinical_guides/esmo/ESMO_PMC11662070_Characterization_of_shared_neoantigens_landscape_i.pdf +3 -0
- data/clinical_guides/esmo/ESMO_PMC11856526_Multiomics_in_silico_analysis_identifies_TM4SF4_as.pdf +3 -0
- data/clinical_guides/esmo/ESMO_PMC12218492_Plain_language_summary_of_the_THOR_Cohort_1_study_.pdf +3 -0
- data/clinical_guides/esmo/ESMO_PMC12306965_Systematic_critical_appraisal_of_GRADE_recommendat.pdf +3 -0
- data/clinical_guides/esmo/ESMO_PMC12381471_Prevention_and_treatment_of_venous_thromboembolism.pdf +3 -0
- data/clinical_guides/esmo/ESMO_PMC12733557_How_to_Read_a_Next_Generation_Sequencing_Report_fo.pdf +3 -0
- data/clinical_guides/esmo/ESMO_PMC12836719_Adoption_of_electronic_patient_reported_outcomes_i.pdf +3 -0
- data/clinical_guides/esmo/ESMO_PMC12925129_Development_and_formative_evaluation_of_a_follow_u.pdf +3 -0
- data/clinical_guides/esmo/ESMO_PMC12982907_Cost_utility_Analysis_of_R_CHOP_vs_CHOP_in_Patient.pdf +3 -0
- data/clinical_guides/esmo/ESMO_PMC7617288_Randomised_Trial_of_No__Short_term__or_Long_term_A.pdf +3 -0
- data/clinical_guides/esmo/ESMO_PMC8267298_An_evaluation_of_the_reporting_quality_in_clinical.pdf +3 -0
- data/clinical_guides/nccn/Patient guidelines/Guidelines for Detection, Prevention, & Risk Reduction/breastcancerscreening-patient.pdf +3 -0
- data/clinical_guides/nccn/Patient guidelines/Guidelines for Detection, Prevention, & Risk Reduction/colorectal-screening-patient.pdf +3 -0
- data/clinical_guides/nccn/Patient guidelines/Guidelines for Detection, Prevention, & Risk Reduction/genetics-patient.pdf +3 -0
- data/clinical_guides/nccn/Patient guidelines/Guidelines for Detection, Prevention, & Risk Reduction/lung_screening-patient.pdf +3 -0
- data/clinical_guides/nccn/Patient guidelines/Guidelines for Detection, Prevention, & Risk Reduction/prostate-screening-patient.pdf +3 -0
- data/clinical_guides/nccn/Patient guidelines/Guidelines for Specific Populations/aya-patient.pdf +3 -0
- data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/GVDH-patient-guideline.pdf +3 -0
- data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/bloodclots-patient.pdf +3 -0
- data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/distress-patient.pdf +3 -0
- data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/fatigue-patient.pdf +3 -0
- data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/immunotherapy-checkpoint-patient.pdf +3 -0
- data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/immunotherapy-se-car-tcell-patient.pdf +3 -0
- data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/low-blood-patient.pdf +3 -0
- data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/nausea-patient.pdf +3 -0
- data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/palliative-patient.pdf +3 -0
- data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/quitting-smoking-patient.pdf +3 -0
.env.production
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================
|
| 2 |
+
# OncoAgent — PRODUCTION Environment (AMD Instinct MI300X)
|
| 3 |
+
# Copy to .env on the GPU droplet before deploying.
|
| 4 |
+
# ============================================================
|
| 5 |
+
|
| 6 |
+
# Set via: export HF_TOKEN=your_token_here
|
| 7 |
+
HF_TOKEN=
|
| 8 |
+
|
| 9 |
+
# --- Hardware ---
|
| 10 |
+
ROCM_PATH=/opt/rocm
|
| 11 |
+
DEVICE=cuda
|
| 12 |
+
HSA_OVERRIDE_GFX_VERSION=9.4.2
|
| 13 |
+
TENSOR_PARALLEL_SIZE=1
|
| 14 |
+
|
| 15 |
+
# --- Model Tier IDs ---
|
| 16 |
+
TIER1_MODEL_ID=Qwen/Qwen3.5-9B
|
| 17 |
+
TIER2_MODEL_ID=Qwen/Qwen3.6-27B
|
| 18 |
+
BASE_MODEL_ID=Qwen/Qwen3.5-9B
|
| 19 |
+
|
| 20 |
+
# --- Inference Backend (Local vLLM) ---
|
| 21 |
+
VLLM_API_BASE=http://localhost:8000/v1
|
| 22 |
+
VLLM_API_KEY=EMPTY
|
| 23 |
+
|
| 24 |
+
# --- Local LoRA Adapters (MI300X Optimized) ---
|
| 25 |
+
USE_LOCAL_ADAPTERS=false
|
| 26 |
+
LOCAL_ADAPTER_PATH=models/oncoagent_adapters/tier1/checkpoint-1000/
|
| 27 |
+
|
| 28 |
+
# --- Logging ---
|
| 29 |
+
LOG_LEVEL=INFO
|
.gitattributes
CHANGED
|
@@ -33,3 +33,169 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
data/clinical_guides/esmo/ESMO_PMC10416694_Developing_a_core_set_of_patient_reported_outcomes.pdf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
data/clinical_guides/esmo/ESMO_PMC10664856_ESMO_ASCO_Recommendations_for_a_Global_Curriculum_.pdf filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
data/clinical_guides/esmo/ESMO_PMC10774906_ESMO_ASCO_Recommendations_for_a_Global_Curriculum_.pdf filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
data/clinical_guides/esmo/ESMO_PMC11574484_Effects_of_Baduanjin_exercise_on_cancer_related_fa.pdf filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
data/clinical_guides/esmo/ESMO_PMC11628549_Research_hotspots_and_trends_in_immunotherapy_for_.pdf filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
data/clinical_guides/esmo/ESMO_PMC11662070_Characterization_of_shared_neoantigens_landscape_i.pdf filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
data/clinical_guides/esmo/ESMO_PMC11856526_Multiomics_in_silico_analysis_identifies_TM4SF4_as.pdf filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
data/clinical_guides/esmo/ESMO_PMC12218492_Plain_language_summary_of_the_THOR_Cohort_1_study_.pdf filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
data/clinical_guides/esmo/ESMO_PMC12306965_Systematic_critical_appraisal_of_GRADE_recommendat.pdf filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
data/clinical_guides/esmo/ESMO_PMC12381471_Prevention_and_treatment_of_venous_thromboembolism.pdf filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
data/clinical_guides/esmo/ESMO_PMC12733557_How_to_Read_a_Next_Generation_Sequencing_Report_fo.pdf filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
data/clinical_guides/esmo/ESMO_PMC12836719_Adoption_of_electronic_patient_reported_outcomes_i.pdf filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
data/clinical_guides/esmo/ESMO_PMC12925129_Development_and_formative_evaluation_of_a_follow_u.pdf filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
data/clinical_guides/esmo/ESMO_PMC12982907_Cost_utility_Analysis_of_R_CHOP_vs_CHOP_in_Patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
data/clinical_guides/esmo/ESMO_PMC7617288_Randomised_Trial_of_No__Short_term__or_Long_term_A.pdf filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
data/clinical_guides/esmo/ESMO_PMC8267298_An_evaluation_of_the_reporting_quality_in_clinical.pdf filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Detection,[[:space:]]Prevention,[[:space:]]&[[:space:]]Risk[[:space:]]Reduction/breastcancerscreening-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Detection,[[:space:]]Prevention,[[:space:]]&[[:space:]]Risk[[:space:]]Reduction/colorectal-screening-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Detection,[[:space:]]Prevention,[[:space:]]&[[:space:]]Risk[[:space:]]Reduction/genetics-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Detection,[[:space:]]Prevention,[[:space:]]&[[:space:]]Risk[[:space:]]Reduction/lung_screening-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Detection,[[:space:]]Prevention,[[:space:]]&[[:space:]]Risk[[:space:]]Reduction/prostate-screening-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Specific[[:space:]]Populations/aya-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Supportive[[:space:]]Care/GVDH-patient-guideline.pdf filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Supportive[[:space:]]Care/bloodclots-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Supportive[[:space:]]Care/distress-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Supportive[[:space:]]Care/fatigue-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Supportive[[:space:]]Care/immunotherapy-checkpoint-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 63 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Supportive[[:space:]]Care/immunotherapy-se-car-tcell-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 64 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Supportive[[:space:]]Care/low-blood-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 65 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Supportive[[:space:]]Care/nausea-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 66 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Supportive[[:space:]]Care/palliative-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 67 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Supportive[[:space:]]Care/quitting-smoking-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 68 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Supportive[[:space:]]Care/survivorship-crl-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 69 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Supportive[[:space:]]Care/survivorship-hl-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 70 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/CBCL-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 71 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/Mantle-patient-guideline.pdf filter=lfs diff=lfs merge=lfs -text
|
| 72 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/PTCL-patient-guideline.pdf filter=lfs diff=lfs merge=lfs -text
|
| 73 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/SCLC-patient-guideline.pdf filter=lfs diff=lfs merge=lfs -text
|
| 74 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/adrenal-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 75 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/all-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 76 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/aml-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 77 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/anal-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 78 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/basal-cell-patient-guideline.pdf filter=lfs diff=lfs merge=lfs -text
|
| 79 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/bladder-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 80 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/bone-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 81 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/brain-gliomas-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 82 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/cervical-patient-guideline.pdf filter=lfs diff=lfs merge=lfs -text
|
| 83 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/cll-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 84 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/cml-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 85 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/colon-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 86 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/ctcl-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 87 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/esophageal-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 88 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/gallandbile-hp-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 89 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/gist-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 90 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/hn-nasopharynx-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 91 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/hn-oral-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 92 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/hn-oropharyngeal-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 93 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/hodgkin-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 94 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/hodgkinlymphomainchildren-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 95 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/inflammatory-breast-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 96 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/kidney-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 97 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/liver-hp-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 98 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/lung-early-stage-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 99 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/lung-metastatic-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 100 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/mds-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 101 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/melanoma-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 102 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/mpm-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 103 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/mpn-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 104 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/myeloma-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 105 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/mzl-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 106 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/neuroendocrine-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 107 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/nhl-diffuse-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 108 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/nhl-follicular-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 109 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/ovarian-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 110 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/pancreatic-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 111 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/pcnsl-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 112 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/ped_all_patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 113 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/prostate-advanced-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 114 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/prostate-early-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 115 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/rectal-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 116 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/sarcoma-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 117 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/small-bowel-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 118 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/squamous_cell-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 119 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/stage_0_breast-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 120 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/stage_iv_breast-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 121 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/stomach-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 122 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/systemic-mastocytosis-patient-guideline.pdf filter=lfs diff=lfs merge=lfs -text
|
| 123 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/thyroid-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 124 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/uterine-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 125 |
+
data/clinical_guides/nccn/Patient[[:space:]]guidelines/Guidelines[[:space:]]for[[:space:]]Treatment[[:space:]]of[[:space:]]Cancer[[:space:]]by[[:space:]]Type/waldenstrom-patient.pdf filter=lfs diff=lfs merge=lfs -text
|
| 126 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/aml.pdf filter=lfs diff=lfs merge=lfs -text
|
| 127 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/ampullary.pdf filter=lfs diff=lfs merge=lfs -text
|
| 128 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/amyloidosis.pdf filter=lfs diff=lfs merge=lfs -text
|
| 129 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/anal.pdf filter=lfs diff=lfs merge=lfs -text
|
| 130 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/appendiceal.pdf filter=lfs diff=lfs merge=lfs -text
|
| 131 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/b-cell.pdf filter=lfs diff=lfs merge=lfs -text
|
| 132 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/bladder.pdf filter=lfs diff=lfs merge=lfs -text
|
| 133 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/bone.pdf filter=lfs diff=lfs merge=lfs -text
|
| 134 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/breast.pdf filter=lfs diff=lfs merge=lfs -text
|
| 135 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/btc.pdf filter=lfs diff=lfs merge=lfs -text
|
| 136 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/castleman.pdf filter=lfs diff=lfs merge=lfs -text
|
| 137 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/cervical.pdf filter=lfs diff=lfs merge=lfs -text
|
| 138 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/cll.pdf filter=lfs diff=lfs merge=lfs -text
|
| 139 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/cml.pdf filter=lfs diff=lfs merge=lfs -text
|
| 140 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/cns.pdf filter=lfs diff=lfs merge=lfs -text
|
| 141 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/colon.pdf filter=lfs diff=lfs merge=lfs -text
|
| 142 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/cutaneous_lymphomas.pdf filter=lfs diff=lfs merge=lfs -text
|
| 143 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/cutaneous_melanoma.pdf filter=lfs diff=lfs merge=lfs -text
|
| 144 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/dfsp.pdf filter=lfs diff=lfs merge=lfs -text
|
| 145 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/esophageal.pdf filter=lfs diff=lfs merge=lfs -text
|
| 146 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/gastric.pdf filter=lfs diff=lfs merge=lfs -text
|
| 147 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/gist.pdf filter=lfs diff=lfs merge=lfs -text
|
| 148 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/gtn.pdf filter=lfs diff=lfs merge=lfs -text
|
| 149 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/hairy_cell.pdf filter=lfs diff=lfs merge=lfs -text
|
| 150 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/hcc.pdf filter=lfs diff=lfs merge=lfs -text
|
| 151 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/head-and-neck.pdf filter=lfs diff=lfs merge=lfs -text
|
| 152 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/histiocytic_neoplasms.pdf filter=lfs diff=lfs merge=lfs -text
|
| 153 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/hodgkins.pdf filter=lfs diff=lfs merge=lfs -text
|
| 154 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/immunotherapy_infographic.pdf filter=lfs diff=lfs merge=lfs -text
|
| 155 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/kaposi.pdf filter=lfs diff=lfs merge=lfs -text
|
| 156 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/kidney.pdf filter=lfs diff=lfs merge=lfs -text
|
| 157 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/mastocytosis.pdf filter=lfs diff=lfs merge=lfs -text
|
| 158 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/mcc.pdf filter=lfs diff=lfs merge=lfs -text
|
| 159 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/mds.pdf filter=lfs diff=lfs merge=lfs -text
|
| 160 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/meso_peritoneal.pdf filter=lfs diff=lfs merge=lfs -text
|
| 161 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/meso_pleural.pdf filter=lfs diff=lfs merge=lfs -text
|
| 162 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/mlne.pdf filter=lfs diff=lfs merge=lfs -text
|
| 163 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/mpn.pdf filter=lfs diff=lfs merge=lfs -text
|
| 164 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/myeloma.pdf filter=lfs diff=lfs merge=lfs -text
|
| 165 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/neuroblastoma.pdf filter=lfs diff=lfs merge=lfs -text
|
| 166 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/neuroendocrine.pdf filter=lfs diff=lfs merge=lfs -text
|
| 167 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/nmsc.pdf filter=lfs diff=lfs merge=lfs -text
|
| 168 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/nscl.pdf filter=lfs diff=lfs merge=lfs -text
|
| 169 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/occult.pdf filter=lfs diff=lfs merge=lfs -text
|
| 170 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/ovarian.pdf filter=lfs diff=lfs merge=lfs -text
|
| 171 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/pancreatic.pdf filter=lfs diff=lfs merge=lfs -text
|
| 172 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/ped_all.pdf filter=lfs diff=lfs merge=lfs -text
|
| 173 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/ped_b-cell.pdf filter=lfs diff=lfs merge=lfs -text
|
| 174 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/ped_cns.pdf filter=lfs diff=lfs merge=lfs -text
|
| 175 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/ped_hodgkin.pdf filter=lfs diff=lfs merge=lfs -text
|
| 176 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/ped_sts.pdf filter=lfs diff=lfs merge=lfs -text
|
| 177 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/penile.pdf filter=lfs diff=lfs merge=lfs -text
|
| 178 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/prostate.pdf filter=lfs diff=lfs merge=lfs -text
|
| 179 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/rectal.pdf filter=lfs diff=lfs merge=lfs -text
|
| 180 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/sarcoma.pdf filter=lfs diff=lfs merge=lfs -text
|
| 181 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/sclc.pdf filter=lfs diff=lfs merge=lfs -text
|
| 182 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/small_bowel.pdf filter=lfs diff=lfs merge=lfs -text
|
| 183 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/squamous.pdf filter=lfs diff=lfs merge=lfs -text
|
| 184 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/t-cell.pdf filter=lfs diff=lfs merge=lfs -text
|
| 185 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/testicular.pdf filter=lfs diff=lfs merge=lfs -text
|
| 186 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/thymic.pdf filter=lfs diff=lfs merge=lfs -text
|
| 187 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/thyroid.pdf filter=lfs diff=lfs merge=lfs -text
|
| 188 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/uterine.pdf filter=lfs diff=lfs merge=lfs -text
|
| 189 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/uveal.pdf filter=lfs diff=lfs merge=lfs -text
|
| 190 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/vaginal.pdf filter=lfs diff=lfs merge=lfs -text
|
| 191 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/vulvar.pdf filter=lfs diff=lfs merge=lfs -text
|
| 192 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/waldenstroms.pdf filter=lfs diff=lfs merge=lfs -text
|
| 193 |
+
data/clinical_guides/nccn/Professional[[:space:]]guidelines/wilms_tumor.pdf filter=lfs diff=lfs merge=lfs -text
|
| 194 |
+
data/clinical_guides/nccn/immunotherapy_infographic.pdf filter=lfs diff=lfs merge=lfs -text
|
| 195 |
+
data/clinical_guides/nccn/nccn_distress_thermometer.pdf filter=lfs diff=lfs merge=lfs -text
|
| 196 |
+
data/clinical_guides/nccn/questions-to-ask-about-cancer-care.pdf filter=lfs diff=lfs merge=lfs -text
|
| 197 |
+
docs/OncoAgent_Official_Paper.pdf filter=lfs diff=lfs merge=lfs -text
|
| 198 |
+
docs/assets/brand/colors/color_palette.png filter=lfs diff=lfs merge=lfs -text
|
| 199 |
+
docs/assets/brand/logo/oncoagent_logo_dark_mode.png filter=lfs diff=lfs merge=lfs -text
|
| 200 |
+
docs/assets/brand/logo/oncoagent_logo_full_color.png filter=lfs diff=lfs merge=lfs -text
|
| 201 |
+
docs/assets/brand/social/twitter_header.png filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================
|
| 2 |
+
# OncoAgent — Production Dockerfile
|
| 3 |
+
# Hardware: AMD Instinct MI300X / ROCm 7.2
|
| 4 |
+
# Serves: vLLM (Qwen3.5-9B + Qwen3.6-27B) + Gradio UI
|
| 5 |
+
# ============================================================
|
| 6 |
+
|
| 7 |
+
# Base image: vLLM optimized for ROCm
|
| 8 |
+
FROM rocm/vllm:latest
|
| 9 |
+
|
| 10 |
+
# System environment
|
| 11 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
| 12 |
+
ENV PYTHONUNBUFFERED=1
|
| 13 |
+
ENV GRADIO_SERVER_NAME="0.0.0.0"
|
| 14 |
+
ENV GRADIO_SERVER_PORT=7860
|
| 15 |
+
|
| 16 |
+
# ROCm / PyTorch environment
|
| 17 |
+
ENV HSA_OVERRIDE_GFX_VERSION=9.4.2
|
| 18 |
+
ENV PYTORCH_ROCM_ARCH="gfx942"
|
| 19 |
+
|
| 20 |
+
# OncoAgent model configuration
|
| 21 |
+
ENV TIER1_MODEL_ID="Qwen/Qwen3.5-9B"
|
| 22 |
+
ENV TIER2_MODEL_ID="Qwen/Qwen3.6-27B"
|
| 23 |
+
ENV BASE_MODEL_ID="Qwen/Qwen3.5-9B"
|
| 24 |
+
ENV VLLM_API_BASE="http://localhost:8000/v1"
|
| 25 |
+
ENV VLLM_API_KEY="EMPTY"
|
| 26 |
+
ENV USE_LOCAL_ADAPTERS="false"
|
| 27 |
+
ENV DEVICE="cuda"
|
| 28 |
+
ENV TENSOR_PARALLEL_SIZE=1
|
| 29 |
+
|
| 30 |
+
WORKDIR /app
|
| 31 |
+
|
| 32 |
+
# System dependencies
|
| 33 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 34 |
+
git \
|
| 35 |
+
build-essential \
|
| 36 |
+
supervisor \
|
| 37 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 38 |
+
|
| 39 |
+
# Python dependencies
|
| 40 |
+
COPY requirements.txt /app/
|
| 41 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 42 |
+
|
| 43 |
+
# Application code
|
| 44 |
+
COPY . /app/
|
| 45 |
+
|
| 46 |
+
# Make deploy scripts executable
|
| 47 |
+
RUN chmod +x deploy/start_vllm.sh
|
| 48 |
+
|
| 49 |
+
# Supervisor config to run vLLM + Gradio simultaneously
|
| 50 |
+
RUN cat > /etc/supervisor/conf.d/oncoagent.conf <<'EOF'
|
| 51 |
+
[supervisord]
|
| 52 |
+
nodaemon=true
|
| 53 |
+
logfile=/var/log/supervisord.log
|
| 54 |
+
|
| 55 |
+
[program:vllm]
|
| 56 |
+
command=bash /app/deploy/start_vllm.sh tier1
|
| 57 |
+
directory=/app
|
| 58 |
+
autostart=true
|
| 59 |
+
autorestart=true
|
| 60 |
+
stdout_logfile=/var/log/vllm.log
|
| 61 |
+
stderr_logfile=/var/log/vllm_err.log
|
| 62 |
+
priority=10
|
| 63 |
+
|
| 64 |
+
[program:gradio]
|
| 65 |
+
command=python /app/ui/app.py
|
| 66 |
+
directory=/app
|
| 67 |
+
autostart=true
|
| 68 |
+
autorestart=true
|
| 69 |
+
stdout_logfile=/var/log/gradio.log
|
| 70 |
+
stderr_logfile=/var/log/gradio_err.log
|
| 71 |
+
priority=20
|
| 72 |
+
startsecs=30
|
| 73 |
+
EOF
|
| 74 |
+
|
| 75 |
+
# Expose ports: Gradio (7860) + vLLM API (8000)
|
| 76 |
+
EXPOSE 7860 8000
|
| 77 |
+
|
| 78 |
+
# Start both services via supervisor
|
| 79 |
+
CMD ["supervisord", "-c", "/etc/supervisor/conf.d/oncoagent.conf"]
|
README.es.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🧬 OncoAgent — Sistema Multi-Agente de Triaje Oncológico
|
| 2 |
+
|
| 3 |
+

|
| 4 |
+

|
| 5 |
+

|
| 6 |
+

|
| 7 |
+

|
| 8 |
+
|
| 9 |
+
> **AMD Developer Hackathon 2026** · Potenciado por AMD Instinct™ MI300X · ROCm 7.2
|
| 10 |
+
|
| 11 |
+
## 🌍 100% Código Abierto: Democratizando la Oncología
|
| 12 |
+
OncoAgent es orgullosamente 100% de código abierto. Creemos que la inteligencia clínica capaz de salvar vidas no debería estar bloqueada tras APIs propietarias. Nuestra solución está diseñada para:
|
| 13 |
+
- **Garantizar la Privacidad del Paciente:** Ejecutarse localmente en hardware AMD MI300X o nubes privadas, asegurando que ningún dato del paciente abandone el hospital.
|
| 14 |
+
- **Fomentar la Contribución Global:** Permitir a las comunidades médicas de todo el mundo auditar, modificar y contribuir fácilmente a la base de conocimiento RAG.
|
| 15 |
+
|
| 16 |
+
OncoAgent es un sistema de triaje clínico multi-agente de última generación diseñado para combatir la **ceguera por datos no estructurados** en la oncología de atención primaria. Aprovecha una arquitectura adaptativa por niveles con modelos **Qwen 3.5-9B** (Triaje Rápido) y **Qwen 3.6-27B** (Razonamiento Profundo). Orquestado a través de una sofisticada máquina de estados de LangGraph, proporciona razonamiento oncológico basado en evidencia estrictamente fundamentado en las guías clínicas de NCCN/ESMO, con puertas de seguridad de validación humana (HITL) integradas y un bucle de crítica basado en Reflexion.
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## 🏗️ Arquitectura
|
| 21 |
+
|
| 22 |
+
```
|
| 23 |
+
┌────────┐ ┌─────────┐ ┌─────────┐ ┌────────────┐ ┌────────────┐ ┌─────────┐
|
| 24 |
+
│ Router │──▶│Ingestión│──▶│ RAG │──▶│Especialista│◀────│ Crítico │ │Formateo │
|
| 25 |
+
│(Triaje)│ │ (PHI) │ │Correctiv│ │ (Qwen 9B/ │ │(Validación │ │(Salida) │
|
| 26 |
+
└────────┘ └─────────┘ └─────────┘ │ 27B) │────▶│ Reflexion) │ └─────────┘
|
| 27 |
+
│ │ │ └────────────┘ └────────────┘ ▲
|
| 28 |
+
│ │ │ │ │ │
|
| 29 |
+
▼ ▼ ▼ ▼ ▼ │
|
| 30 |
+
┌───────────────────────────────────────────────────────────────────┐ ┌────────────┐
|
| 31 |
+
│ Nodo de Respaldo (Fallback) │ │Puerta HITL │
|
| 32 |
+
└───────────────────────────────────────────────────────────────────┘ │(Agudeza) │
|
| 33 |
+
└────────────┘
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
**Componentes Principales:**
|
| 37 |
+
|
| 38 |
+
| Módulo | Descripción |
|
| 39 |
+
|--------|-------------|
|
| 40 |
+
| `data_prep/` | Constructor del dataset: PMC-Patients/OncoCoT → Strict JSONL (Plantilla chat de Llama 3) |
|
| 41 |
+
| `rag_engine/` | El "Cerebro": Extracción PyMuPDF, Semantic Chunking Adaptativo de PDFs NCCN/ESMO, & Vectorización ChromaDB + PubMedBERT. |
|
| 42 |
+
| `agents/` | El "Razonamiento": Orquestación multi-agente LangGraph (Router → Corrective RAG → Specialist ↔ Critic → HITL Gate). |
|
| 43 |
+
| `ui/` | La "Cara": Interfaz Gradio 6 con Glassmorphism para input clínico, citas en tiempo real y salida estructurada. |
|
| 44 |
+
|
| 45 |
+
---
|
| 46 |
+
|
| 47 |
+
## 🧠 Estrategia de Modelo Dual-Tier (Qwen)
|
| 48 |
+
|
| 49 |
+
Para maximizar las capacidades de cómputo del **AMD MI300X**, OncoAgent implementa una estrategia de enrutamiento dinámica de **Doble Nivel (Dual-Tier)** utilizando la familia de modelos Qwen. **Ambos niveles (tiers) han sido ajustados (fine-tuned) en más de 200,000 casos oncológicos clínicos reales, cubriendo todos los tipos principales de cáncer** (derivados de los datasets PMC-Patients y OncoCoT) para garantizar un razonamiento médico hiper-especializado:
|
| 50 |
+
|
| 51 |
+
- **Tier 1: Qwen 3.5-9B (Speed Triage):** Un modelo extremadamente rápido y ligero usado por el `Router` para evaluar la complejidad inicial, realizar triaje simple y procesar consultas de bajo riesgo.
|
| 52 |
+
- **Tier 2: Qwen 3.6-27B (Deep Reasoning):** El modelo pesado. Se activa para casos clínicos de alta complejidad (ej. metástasis, mutaciones múltiples). Realiza un razonamiento profundo y verificaciones de entrelazamiento (entailment checks), evitando el sesgo de confirmación mediante rigurosos bucles de Reflexion.
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
## ⚡ Objetivo de Hardware
|
| 57 |
+
|
| 58 |
+
- **GPU:** AMD Instinct™ MI300X (192GB HBM3)
|
| 59 |
+
- **Pila de Software:** ROCm 7.2.x, PyTorch (HIP), vLLM con PagedAttention
|
| 60 |
+
- **Modelos:** `Qwen/Qwen3.5-9B` (Triaje Rápido) y `Qwen/Qwen3.6-27B-Instruct` (Razonamiento Profundo)
|
| 61 |
+
- **Precisión:** QLoRA 4-bit NormalFloat4 vía `bitsandbytes` (Compatible con ROCm)
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## 🚀 Inicio Rápido
|
| 66 |
+
|
| 67 |
+
```bash
|
| 68 |
+
# 1. Clonar y configurar
|
| 69 |
+
git clone <repo-url>
|
| 70 |
+
cd OncoAgent
|
| 71 |
+
|
| 72 |
+
# 2. Instalar dependencias
|
| 73 |
+
python -m venv .venv
|
| 74 |
+
source .venv/bin/activate
|
| 75 |
+
pip install -r requirements.txt
|
| 76 |
+
|
| 77 |
+
# 3. Iniciar Servidor de Inferencia (vLLM en Docker)
|
| 78 |
+
# Esto levanta los modelos Qwen optimizados para AMD MI300X vía ROCm PagedAttention
|
| 79 |
+
docker run --device /dev/kfd --device /dev/dri -p 8000:8000 rocm/vllm:latest \
|
| 80 |
+
--model Qwen/Qwen3.6-27B-Instruct --tensor-parallel-size 1
|
| 81 |
+
|
| 82 |
+
# 4. Configurar entorno y ejecutar interfaz
|
| 83 |
+
cp .env.example .env
|
| 84 |
+
# Configurar VLLM_API_BASE=http://localhost:8000/v1 en .env
|
| 85 |
+
python -m ui.app
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
---
|
| 89 |
+
|
| 90 |
+
## 📁 Estructura del Proyecto
|
| 91 |
+
|
| 92 |
+
```
|
| 93 |
+
├── docs/ # Documentación e investigación
|
| 94 |
+
│ ├── research/ # Documentos de análisis de investigación profunda
|
| 95 |
+
│ ├── ADR/ # Registros de Decisiones Arquitectónicas (ADRs)
|
| 96 |
+
│ ├── oncoagent_master_directive.md
|
| 97 |
+
│ └── antigravity_rules.md
|
| 98 |
+
├── data_prep/ # Preparación de conjuntos de datos (Fase 0)
|
| 99 |
+
├── rag_engine/ # Ingestión y recuperación de RAG (Fase 0-3)
|
| 100 |
+
├── agents/ # Orquestación LangGraph (Fase 3)
|
| 101 |
+
├── ui/ # Frontend en Gradio (Fase 4)
|
| 102 |
+
├── tests/ # Pruebas unitarias e integración
|
| 103 |
+
├── scripts/ # Scripts de utilidad
|
| 104 |
+
├── logs/ # Bitácora (Paper log) y de redes sociales
|
| 105 |
+
├── requirements.txt # Dependencias fijadas
|
| 106 |
+
└── Dockerfile # Despliegue en HF Spaces
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
---
|
| 110 |
+
|
| 111 |
+
## 🩺 Garantías de Seguridad
|
| 112 |
+
|
| 113 |
+
- **Bucle Crítico basado en Reflexion:** Un nodo de seguridad dedicado audita la salida del Especialista contra el contexto RAG (verificación de implicación). Obliga al Especialista a regenerar su salida si detecta afirmaciones sin fundamento o dosis inventadas.
|
| 114 |
+
- **Puerta de Aprobación Humana (HITL):** Un punto de control basado en la agudeza clínica que detiene el flujo para la aprobación de un médico humano en casos de alto riesgo (ej. Estadio IV + mutaciones complejas).
|
| 115 |
+
- **RAG Correctivo:** El sistema evalúa la relevancia del contexto recuperado. Si no se encuentra evidencia suficiente, se activa un respaldo seguro en lugar de intentar adivinar.
|
| 116 |
+
- **Cero-PHI (Cero Información Médica Privada):** Redacción de PII basada en expresiones regulares antes de cualquier procesamiento.
|
| 117 |
+
- **Reproducibilidad:** Semillas fijas (`torch.manual_seed(42)`) en todos los scripts de ML.
|
| 118 |
+
|
| 119 |
+
---
|
| 120 |
+
|
| 121 |
+
## 📄 Licencia
|
| 122 |
+
|
| 123 |
+
Este proyecto fue construido para el AMD Developer Hackathon 2026.
|
| 124 |
+
|
| 125 |
+
---
|
| 126 |
+
|
| 127 |
+
## 👥 Equipo
|
| 128 |
+
|
| 129 |
+
Construido con ❤️ y AMD Instinct MI300X.
|
README.md
CHANGED
|
@@ -6,9 +6,9 @@ colorTo: blue
|
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 5.31.0
|
| 8 |
app_file: app.py
|
| 9 |
-
pinned:
|
| 10 |
license: apache-2.0
|
| 11 |
-
short_description: Multi-
|
| 12 |
---
|
| 13 |
|
| 14 |
# 🧬 OncoAgent — Multi-Agent Oncology Triage System
|
|
@@ -139,4 +139,4 @@ This project was built for the AMD Developer Hackathon 2026.
|
|
| 139 |
|
| 140 |
## 👥 Team
|
| 141 |
|
| 142 |
-
Built with ❤️ and AMD Instinct MI300X.
|
|
|
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 5.31.0
|
| 8 |
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
license: apache-2.0
|
| 11 |
+
short_description: Multi-Agent Oncology Triage powered by AMD MI300X
|
| 12 |
---
|
| 13 |
|
| 14 |
# 🧬 OncoAgent — Multi-Agent Oncology Triage System
|
|
|
|
| 139 |
|
| 140 |
## 👥 Team
|
| 141 |
|
| 142 |
+
Built with ❤️ and AMD Instinct MI300X.
|
agents/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
OncoAgent Multi-Agent System — SOTA Architecture.
|
| 3 |
+
|
| 4 |
+
This package implements a production-grade clinical oncology triage system
|
| 5 |
+
using LangGraph for orchestration, incorporating:
|
| 6 |
+
|
| 7 |
+
- Router: complexity classification + model tier selection
|
| 8 |
+
- Corrective RAG: graded retrieval with query rewriting
|
| 9 |
+
- Specialist: tier-adaptive clinical reasoning (9B/27B)
|
| 10 |
+
- Critic: reflexion-pattern validation loop
|
| 11 |
+
- HITL Gate: clinician approval for high-acuity cases
|
| 12 |
+
- Formatter: structured output with confidence metrics
|
| 13 |
+
|
| 14 |
+
Architecture inspired by Claude Code, Hermes Agent, Corrective RAG,
|
| 15 |
+
and Reflexion patterns.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from .graph import build_oncoagent_graph
|
| 19 |
+
from .state import AgentState
|
| 20 |
+
from .memory import get_memory_store, PatientMemoryStore
|
| 21 |
+
from .tools import get_vllm_client, call_tier_model, get_tier_spec, TIER_SPECS
|
| 22 |
+
|
| 23 |
+
__all__ = [
|
| 24 |
+
"build_oncoagent_graph",
|
| 25 |
+
"AgentState",
|
| 26 |
+
"get_memory_store",
|
| 27 |
+
"PatientMemoryStore",
|
| 28 |
+
"get_vllm_client",
|
| 29 |
+
"call_tier_model",
|
| 30 |
+
"get_tier_spec",
|
| 31 |
+
"TIER_SPECS",
|
| 32 |
+
]
|
agents/corrective_rag.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Corrective RAG Node — Graded retrieval with query rewriting.
|
| 3 |
+
|
| 4 |
+
Design pattern: Corrective RAG (CRAG) from Yan et al. 2024
|
| 5 |
+
1. Retrieve top-K documents from ChromaDB
|
| 6 |
+
2. Grade each document for relevance (binary: RELEVANT / IRRELEVANT)
|
| 7 |
+
3. If insufficient relevant docs → rewrite query and re-retrieve
|
| 8 |
+
4. If still insufficient after max retries → route to fallback
|
| 9 |
+
|
| 10 |
+
Also implements parallelised evidence gathering:
|
| 11 |
+
- ChromaDB (clinical guidelines)
|
| 12 |
+
- CIViC API (genomic evidence)
|
| 13 |
+
- ClinicalTrials.gov (active trials)
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import logging
|
| 17 |
+
import re
|
| 18 |
+
from typing import Dict, Any, List, Optional
|
| 19 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 20 |
+
|
| 21 |
+
from .state import AgentState
|
| 22 |
+
from .tools import call_tier_model
|
| 23 |
+
|
| 24 |
+
logger = logging.getLogger(__name__)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# ---------------------------------------------------------------------------
|
| 28 |
+
# Lazy-loaded retriever singleton
|
| 29 |
+
# ---------------------------------------------------------------------------
|
| 30 |
+
|
| 31 |
+
_retriever_instance = None
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _get_retriever():
|
| 35 |
+
"""Return a cached OncoRAGRetriever instance (lazy init)."""
|
| 36 |
+
global _retriever_instance
|
| 37 |
+
if _retriever_instance is None:
|
| 38 |
+
try:
|
| 39 |
+
from rag_engine.retriever import OncoRAGRetriever
|
| 40 |
+
_retriever_instance = OncoRAGRetriever()
|
| 41 |
+
logger.info("OncoRAGRetriever initialised successfully.")
|
| 42 |
+
except Exception as exc:
|
| 43 |
+
logger.error("Failed to initialise OncoRAGRetriever: %s", exc)
|
| 44 |
+
raise
|
| 45 |
+
return _retriever_instance
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# ---------------------------------------------------------------------------
|
| 49 |
+
# Document Grading (CRAG core)
|
| 50 |
+
# ---------------------------------------------------------------------------
|
| 51 |
+
|
| 52 |
+
def _grade_document(document_text: str, query: str, tier: int = 1) -> bool:
|
| 53 |
+
"""Grade a single retrieved document for relevance.
|
| 54 |
+
|
| 55 |
+
Uses the Tier 1 (fast) model for binary classification.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
document_text: The document text to evaluate.
|
| 59 |
+
query: The clinical query.
|
| 60 |
+
tier: Model tier to use for grading (default: 1 for speed).
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
True if the document is RELEVANT, False otherwise.
|
| 64 |
+
"""
|
| 65 |
+
# Rule-based shortcut: if the query contains a core cancer type and the document mentions it,
|
| 66 |
+
# we favor relevance to avoid model hallucination/rejection.
|
| 67 |
+
query_lower = query.lower()
|
| 68 |
+
doc_lower = document_text.lower()
|
| 69 |
+
|
| 70 |
+
# Simple semantic overlap check: ignore generic terms
|
| 71 |
+
ignore_terms = {"treatment", "recommendation", "guidelines", "triage", "management", "clinical", "oncology"}
|
| 72 |
+
core_terms = [t for t in query_lower.split() if len(t) > 4 and t not in ignore_terms]
|
| 73 |
+
term_match = any(term in doc_lower for term in core_terms)
|
| 74 |
+
|
| 75 |
+
system_prompt = (
|
| 76 |
+
"You are an expert Oncology Clinical Analyst. "
|
| 77 |
+
"Your task is to evaluate if a retrieved medical document is RELEVANT to a patient's clinical query. "
|
| 78 |
+
"RELEVANCE CRITERIA:\n"
|
| 79 |
+
"1. The document discusses the specific cancer type or its precursors.\n"
|
| 80 |
+
"2. The document mentions treatment protocols, staging, or diagnostic criteria relevant to the case.\n"
|
| 81 |
+
"3. Synonyms are allowed (e.g., 'Uterine Cancer' is relevant to 'Endometrial Adenocarcinoma').\n\n"
|
| 82 |
+
"Output ONLY 'RELEVANT' or 'IRRELEVANT'. No explanation."
|
| 83 |
+
)
|
| 84 |
+
user_prompt = (
|
| 85 |
+
f"Patient Query/Context: {query}\n\n"
|
| 86 |
+
f"Document Snippet:\n--- START ---\n{document_text[:2000]}\n--- END ---\n\n"
|
| 87 |
+
f"Is this document relevant? (RELEVANT/IRRELEVANT):"
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
try:
|
| 91 |
+
response = call_tier_model(
|
| 92 |
+
tier=tier,
|
| 93 |
+
system_prompt=system_prompt,
|
| 94 |
+
user_prompt=user_prompt,
|
| 95 |
+
max_tokens=10,
|
| 96 |
+
temperature=0.0,
|
| 97 |
+
)
|
| 98 |
+
is_relevant = "RELEVANT" in response.upper()
|
| 99 |
+
logger.info("Doc Grade: %s (Term Match: %s) -> Query: %s...", "RELEVANT" if is_relevant else "IRRELEVANT", term_match, query[:30])
|
| 100 |
+
|
| 101 |
+
# Boost: if model says IRRELEVANT but there is a strong term match, we might want to override.
|
| 102 |
+
# This ensures we don't drop guidelines that explicitly mention the cancer type.
|
| 103 |
+
if not is_relevant and term_match:
|
| 104 |
+
logger.debug("Model rejected doc but keyword match found. Overriding to RELEVANT for recall.")
|
| 105 |
+
return True
|
| 106 |
+
|
| 107 |
+
return is_relevant
|
| 108 |
+
except Exception as exc:
|
| 109 |
+
logger.warning("Document grading failed: %s — defaulting to RELEVANT.", exc)
|
| 110 |
+
return True # Fail open: include document if grading fails
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def _rewrite_query(
|
| 115 |
+
original_query: str,
|
| 116 |
+
entities: Dict[str, Any],
|
| 117 |
+
attempt: int,
|
| 118 |
+
) -> str:
|
| 119 |
+
"""Broaden the query for a retry attempt.
|
| 120 |
+
|
| 121 |
+
Uses deterministic broadening rather than LLM-based rewriting
|
| 122 |
+
for speed and predictability.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
original_query: The query that yielded insufficient results.
|
| 126 |
+
entities: Extracted clinical entities.
|
| 127 |
+
attempt: The retry attempt number (1-indexed).
|
| 128 |
+
|
| 129 |
+
Returns:
|
| 130 |
+
A broadened query string.
|
| 131 |
+
"""
|
| 132 |
+
cancer = entities.get("cancer_type", "Unknown")
|
| 133 |
+
stage = entities.get("stage", "Unknown")
|
| 134 |
+
mutations = entities.get("mutations", [])
|
| 135 |
+
|
| 136 |
+
if attempt == 1:
|
| 137 |
+
# Broadening strategy: remove stage specificity, keep cancer + mutations
|
| 138 |
+
parts = [cancer]
|
| 139 |
+
if mutations:
|
| 140 |
+
parts.append(f"mutations {' '.join(mutations)}")
|
| 141 |
+
parts.append("treatment guidelines evidence-based recommendations")
|
| 142 |
+
rewritten = " ".join(parts)
|
| 143 |
+
logger.info("Query rewrite attempt %d: %s → %s", attempt, original_query, rewritten)
|
| 144 |
+
return rewritten
|
| 145 |
+
|
| 146 |
+
# Attempt 2+: maximally broad
|
| 147 |
+
rewritten = f"{cancer} oncology clinical guidelines management"
|
| 148 |
+
logger.info("Query rewrite attempt %d (maximal broadening): %s", attempt, rewritten)
|
| 149 |
+
return rewritten
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
# ---------------------------------------------------------------------------
|
| 153 |
+
# Parallel Evidence Gathering
|
| 154 |
+
# ---------------------------------------------------------------------------
|
| 155 |
+
|
| 156 |
+
def _fetch_api_evidence(entities: Dict[str, Any]) -> Dict[str, List[str]]:
|
| 157 |
+
"""Fetch genomic and clinical trial evidence in parallel.
|
| 158 |
+
|
| 159 |
+
Calls CIViC API and ClinicalTrials.gov concurrently for MI300X
|
| 160 |
+
throughput optimisation.
|
| 161 |
+
|
| 162 |
+
Args:
|
| 163 |
+
entities: Extracted clinical entities.
|
| 164 |
+
|
| 165 |
+
Returns:
|
| 166 |
+
Dict with "genomic_evidence" and "clinical_trials" lists.
|
| 167 |
+
"""
|
| 168 |
+
results: Dict[str, List[str]] = {
|
| 169 |
+
"genomic_evidence": [],
|
| 170 |
+
"clinical_trials": [],
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
mutations = entities.get("mutations", [])
|
| 174 |
+
cancer = entities.get("cancer_type", "Unknown")
|
| 175 |
+
|
| 176 |
+
def fetch_civic():
|
| 177 |
+
"""Fetch genomic evidence from CIViC."""
|
| 178 |
+
try:
|
| 179 |
+
from rag_engine.api_clients import CivicAPIClient
|
| 180 |
+
client = CivicAPIClient()
|
| 181 |
+
evidence = []
|
| 182 |
+
for mutation in mutations:
|
| 183 |
+
civic_results = client.search_variant_evidence(mutation, cancer)
|
| 184 |
+
for r in civic_results:
|
| 185 |
+
evidence.append(
|
| 186 |
+
f"[CIViC] {mutation}: {r.get('summary', 'No summary available')}"
|
| 187 |
+
)
|
| 188 |
+
return evidence
|
| 189 |
+
except Exception as exc:
|
| 190 |
+
logger.warning("CIViC API failed: %s", exc)
|
| 191 |
+
return []
|
| 192 |
+
|
| 193 |
+
def fetch_trials():
|
| 194 |
+
"""Fetch active clinical trials."""
|
| 195 |
+
try:
|
| 196 |
+
from rag_engine.api_clients import ClinicalTrialsClient
|
| 197 |
+
client = ClinicalTrialsClient()
|
| 198 |
+
trial_results = client.search_trials(cancer, mutations)
|
| 199 |
+
return [
|
| 200 |
+
f"[ClinicalTrials.gov] {t.get('title', 'Unknown')}: {t.get('status', '?')}"
|
| 201 |
+
for t in trial_results
|
| 202 |
+
]
|
| 203 |
+
except Exception as exc:
|
| 204 |
+
logger.warning("ClinicalTrials.gov API failed: %s", exc)
|
| 205 |
+
return []
|
| 206 |
+
|
| 207 |
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
| 208 |
+
futures = {
|
| 209 |
+
executor.submit(fetch_civic): "genomic_evidence",
|
| 210 |
+
executor.submit(fetch_trials): "clinical_trials",
|
| 211 |
+
}
|
| 212 |
+
for future in as_completed(futures):
|
| 213 |
+
key = futures[future]
|
| 214 |
+
try:
|
| 215 |
+
results[key] = future.result()
|
| 216 |
+
except Exception as exc:
|
| 217 |
+
logger.error("Parallel fetch error (%s): %s", key, exc)
|
| 218 |
+
|
| 219 |
+
return results
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
# ---------------------------------------------------------------------------
|
| 223 |
+
# Corrective RAG Node
|
| 224 |
+
# ---------------------------------------------------------------------------
|
| 225 |
+
|
| 226 |
+
# Minimum relevant documents required to proceed
|
| 227 |
+
_MIN_RELEVANT_DOCS = 2
|
| 228 |
+
# Maximum query rewrite attempts
|
| 229 |
+
_MAX_REWRITES = 1
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def corrective_rag_node(state: AgentState) -> Dict[str, Any]:
|
| 233 |
+
"""Execute the Corrective RAG pipeline.
|
| 234 |
+
|
| 235 |
+
Pipeline:
|
| 236 |
+
1. Build structured query from extracted entities.
|
| 237 |
+
2. Retrieve top-K candidates from ChromaDB.
|
| 238 |
+
3. Grade each document for relevance.
|
| 239 |
+
4. If insufficient relevant docs → rewrite query and retry.
|
| 240 |
+
5. Fetch API evidence in parallel (CIViC + ClinicalTrials).
|
| 241 |
+
6. Compute confidence metrics.
|
| 242 |
+
|
| 243 |
+
Args:
|
| 244 |
+
state: Current LangGraph state.
|
| 245 |
+
|
| 246 |
+
Returns:
|
| 247 |
+
State update with rag_context, sources, confidence, and metrics.
|
| 248 |
+
"""
|
| 249 |
+
entities: Dict[str, Any] = state.get("extracted_entities", {})
|
| 250 |
+
clinical_text: str = state.get("clinical_text", "")
|
| 251 |
+
selected_tier: int = state.get("selected_tier", 1)
|
| 252 |
+
|
| 253 |
+
# --- Build initial query ---
|
| 254 |
+
cancer = entities.get("cancer_type", "Unknown")
|
| 255 |
+
stage = entities.get("stage", "Unknown")
|
| 256 |
+
mutations = ", ".join(entities.get("mutations", []))
|
| 257 |
+
|
| 258 |
+
query_parts = []
|
| 259 |
+
if cancer != "Unknown":
|
| 260 |
+
query_parts.append(cancer)
|
| 261 |
+
else:
|
| 262 |
+
# Fallback: use first 100 chars of clinical text for vector search
|
| 263 |
+
query_parts.append(clinical_text[:100].replace("\n", " "))
|
| 264 |
+
|
| 265 |
+
if stage != "Unknown":
|
| 266 |
+
query_parts.append(stage)
|
| 267 |
+
if mutations:
|
| 268 |
+
query_parts.append(f"mutations: {mutations}")
|
| 269 |
+
query_parts.append("treatment recommendation guidelines triage")
|
| 270 |
+
query = " ".join(query_parts)
|
| 271 |
+
|
| 272 |
+
rewrite_count = 0
|
| 273 |
+
relevant_docs: List[Dict[str, Any]] = []
|
| 274 |
+
|
| 275 |
+
try:
|
| 276 |
+
retriever = _get_retriever()
|
| 277 |
+
|
| 278 |
+
# --- Retrieve + Grade loop ---
|
| 279 |
+
for attempt in range(1 + _MAX_REWRITES):
|
| 280 |
+
if attempt > 0:
|
| 281 |
+
query = _rewrite_query(query, entities, attempt)
|
| 282 |
+
rewrite_count += 1
|
| 283 |
+
|
| 284 |
+
# Retrieve candidates
|
| 285 |
+
raw_results = retriever.query(query, n_results=8)
|
| 286 |
+
|
| 287 |
+
# Grade documents in parallel for MI300X/API efficiency
|
| 288 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 289 |
+
|
| 290 |
+
def _grade_doc_wrapper(r):
|
| 291 |
+
doc_text = r.get("text", "")
|
| 292 |
+
is_relevant = _grade_document(doc_text, query, tier=1)
|
| 293 |
+
return r if is_relevant else None
|
| 294 |
+
|
| 295 |
+
with ThreadPoolExecutor(max_workers=8) as executor:
|
| 296 |
+
results = list(executor.map(_grade_doc_wrapper, raw_results))
|
| 297 |
+
|
| 298 |
+
graded = [r for r in results if r is not None]
|
| 299 |
+
|
| 300 |
+
logger.info(
|
| 301 |
+
"CRAG attempt %d: %d/%d documents graded RELEVANT (Parallel).",
|
| 302 |
+
attempt + 1, len(graded), len(raw_results),
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
if len(graded) >= _MIN_RELEVANT_DOCS:
|
| 306 |
+
relevant_docs = graded
|
| 307 |
+
break
|
| 308 |
+
|
| 309 |
+
# --- Format results ---
|
| 310 |
+
context_strings = []
|
| 311 |
+
source_strings = []
|
| 312 |
+
for r in relevant_docs:
|
| 313 |
+
context_strings.append(
|
| 314 |
+
f"[Source: {r['source']}, Page: {r.get('page', '?')}, "
|
| 315 |
+
f"Section: {r.get('header', 'Unknown')}]\n{r['text']}"
|
| 316 |
+
)
|
| 317 |
+
source_strings.append(
|
| 318 |
+
f"- **{r['source']}** (Page {r.get('page', '?')}): "
|
| 319 |
+
f"{r.get('header', 'Unknown')}"
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
# --- Confidence metrics ---
|
| 323 |
+
ce_scores = [
|
| 324 |
+
r["cross_encoder_score"]
|
| 325 |
+
for r in relevant_docs
|
| 326 |
+
if "cross_encoder_score" in r
|
| 327 |
+
]
|
| 328 |
+
mean_confidence = sum(ce_scores) / len(ce_scores) if ce_scores else 0.0
|
| 329 |
+
|
| 330 |
+
except Exception as exc:
|
| 331 |
+
logger.error("RAG retrieval failed: %s", exc)
|
| 332 |
+
context_strings = []
|
| 333 |
+
source_strings = []
|
| 334 |
+
relevant_docs = []
|
| 335 |
+
mean_confidence = 0.0
|
| 336 |
+
rewrite_count = 0
|
| 337 |
+
|
| 338 |
+
# --- Parallel API evidence ---
|
| 339 |
+
api_results = _fetch_api_evidence(entities)
|
| 340 |
+
|
| 341 |
+
return {
|
| 342 |
+
"rag_context": context_strings,
|
| 343 |
+
"rag_sources": source_strings,
|
| 344 |
+
"graph_rag_context": [], # Future: knowledge graph integration
|
| 345 |
+
"api_evidence_context": (
|
| 346 |
+
api_results.get("genomic_evidence", [])
|
| 347 |
+
+ api_results.get("clinical_trials", [])
|
| 348 |
+
),
|
| 349 |
+
"rag_confidence": round(mean_confidence, 4),
|
| 350 |
+
"rag_retrieval_count": len(context_strings),
|
| 351 |
+
"rag_grading_pass_count": len(relevant_docs),
|
| 352 |
+
"rag_query_rewrites": rewrite_count,
|
| 353 |
+
}
|
agents/critic.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Critic Node — Reflexion-pattern validation for clinical recommendations.
|
| 3 |
+
|
| 4 |
+
Design pattern: Reflexion (Shinn et al. 2023)
|
| 5 |
+
- Generator (Specialist) → Critic loop
|
| 6 |
+
- Critic evaluates: entailment, completeness, formatting
|
| 7 |
+
- If FAIL → specific feedback injected back to Specialist for retry
|
| 8 |
+
- Max 2 iterations before safe fallback
|
| 9 |
+
|
| 10 |
+
Layer 1: Rule-based checks (deterministic, no LLM needed)
|
| 11 |
+
Layer 2: LLM-based entailment verification (Tier 1 for speed)
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import logging
|
| 15 |
+
import re
|
| 16 |
+
from typing import Dict, Any, List
|
| 17 |
+
|
| 18 |
+
from .state import AgentState
|
| 19 |
+
from .tools import call_tier_model
|
| 20 |
+
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Maximum critic attempts before triggering safe fallback
|
| 25 |
+
MAX_CRITIC_ATTEMPTS = 2
|
| 26 |
+
|
| 27 |
+
# Required semantic concepts in a well-formed recommendation.
|
| 28 |
+
# Each entry is a list of synonyms — at least ONE must appear.
|
| 29 |
+
_REQUIRED_CONCEPTS = [
|
| 30 |
+
# Clinical findings / presentation
|
| 31 |
+
["hallazgos", "findings", "presentación", "presentation", "clinical findings"],
|
| 32 |
+
# Diagnostic validation
|
| 33 |
+
["diagnóstic", "diagnostic", "validación", "biopsia", "biopsy", "patholog", "patolog"],
|
| 34 |
+
# Management / treatment options
|
| 35 |
+
["manejo", "management", "tratamiento", "treatment", "opciones", "options", "histerectom", "hysterectom", "surgery", "cirugía"],
|
| 36 |
+
# Final recommendation
|
| 37 |
+
["recomendación", "recommendation", "conclusi", "next step"],
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# ---------------------------------------------------------------------------
|
| 42 |
+
# Layer 1: Deterministic checks (no LLM)
|
| 43 |
+
# ---------------------------------------------------------------------------
|
| 44 |
+
|
| 45 |
+
def _check_formatting(recommendation: str) -> tuple[bool, str]:
|
| 46 |
+
"""Verify the recommendation contains required structural sections.
|
| 47 |
+
|
| 48 |
+
Uses flexible semantic matching instead of exact section headers,
|
| 49 |
+
so the model can use different header styles and still pass.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
recommendation: The specialist's output text.
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
Tuple of (passed, feedback_message).
|
| 56 |
+
"""
|
| 57 |
+
text_lower = recommendation.lower()
|
| 58 |
+
missing_concepts = []
|
| 59 |
+
|
| 60 |
+
for synonyms in _REQUIRED_CONCEPTS:
|
| 61 |
+
if not any(syn in text_lower for syn in synonyms):
|
| 62 |
+
missing_concepts.append(synonyms[0])
|
| 63 |
+
|
| 64 |
+
if missing_concepts:
|
| 65 |
+
feedback = (
|
| 66 |
+
f"FORMATTING: Missing required concepts: {', '.join(missing_concepts)}. "
|
| 67 |
+
"Please include all sections: Hallazgos Clínicos, Validación Diagnóstica, "
|
| 68 |
+
"Análisis de Estadificación, Opciones de Manejo, Recomendación Final."
|
| 69 |
+
)
|
| 70 |
+
return False, feedback
|
| 71 |
+
|
| 72 |
+
return True, ""
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _check_safety_phrases(recommendation: str) -> tuple[bool, str]:
|
| 76 |
+
"""Check for known unsafe patterns (e.g., inventing dosages without sources).
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
recommendation: The specialist's output text.
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
Tuple of (passed, feedback_message).
|
| 83 |
+
"""
|
| 84 |
+
# Detect unsupported dosage patterns without source citations
|
| 85 |
+
dosage_pattern = re.compile(
|
| 86 |
+
r"\b\d+\s*(mg|mg/m2|mg/kg|mcg|IU|units)\b",
|
| 87 |
+
re.IGNORECASE,
|
| 88 |
+
)
|
| 89 |
+
dosages_found = dosage_pattern.findall(recommendation)
|
| 90 |
+
|
| 91 |
+
if dosages_found and "[source" not in recommendation.lower():
|
| 92 |
+
return False, (
|
| 93 |
+
"SAFETY: Specific dosages were mentioned without explicit source citations. "
|
| 94 |
+
"Either cite the guideline source for each dosage or remove the specific numbers."
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
return True, ""
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def _check_diagnostic_rigor(recommendation: str, clinical_text: str) -> tuple[bool, str]:
|
| 101 |
+
"""Ensure no premature treatment is recommended without a confirmed diagnosis.
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
recommendation: The specialist's output text.
|
| 105 |
+
clinical_text: The original clinical input.
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
Tuple of (passed, feedback_message).
|
| 109 |
+
"""
|
| 110 |
+
text_lower = clinical_text.lower()
|
| 111 |
+
rec_lower = recommendation.lower()
|
| 112 |
+
|
| 113 |
+
# Detect if a biopsy/pathology was mentioned in the clinical text
|
| 114 |
+
pathology_keywords = [
|
| 115 |
+
"biopsia", "patología", "pathology", "biopsy", "histolog",
|
| 116 |
+
"legrado", "malign", "adenocarcinoma", "carcinoma", "sarcoma",
|
| 117 |
+
"linfoma", "lymphoma", "melanoma", "confirms", "confirma",
|
| 118 |
+
"diagnosed", "diagnosticado",
|
| 119 |
+
]
|
| 120 |
+
has_pathology = any(word in text_lower for word in pathology_keywords)
|
| 121 |
+
|
| 122 |
+
# Treatment keywords
|
| 123 |
+
treatment_keywords = [
|
| 124 |
+
"cirugía", "radioterapia", "quimioterapia", "surgery",
|
| 125 |
+
"radiation", "chemotherapy", "histerectomía", "hysterectomy",
|
| 126 |
+
]
|
| 127 |
+
|
| 128 |
+
if not has_pathology:
|
| 129 |
+
found_treatments = [kw for kw in treatment_keywords if kw in rec_lower]
|
| 130 |
+
if found_treatments:
|
| 131 |
+
feedback = (
|
| 132 |
+
f"DIAGNOSTIC RIGOR: Recommended treatments ({', '.join(found_treatments)}) "
|
| 133 |
+
"but no pathology/biopsy confirmation was found in the clinical text. "
|
| 134 |
+
"You MUST request a diagnostic procedure (e.g., biopsy) first."
|
| 135 |
+
)
|
| 136 |
+
return False, feedback
|
| 137 |
+
|
| 138 |
+
return True, ""
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
# ---------------------------------------------------------------------------
|
| 142 |
+
# Layer 2: LLM-based entailment check
|
| 143 |
+
# ---------------------------------------------------------------------------
|
| 144 |
+
|
| 145 |
+
def _check_entailment(
|
| 146 |
+
recommendation: str,
|
| 147 |
+
context: List[str],
|
| 148 |
+
) -> tuple[bool, str]:
|
| 149 |
+
"""Verify the recommendation is entailed by the RAG context.
|
| 150 |
+
|
| 151 |
+
Uses Tier 1 (fast model) for binary entailment classification.
|
| 152 |
+
|
| 153 |
+
Args:
|
| 154 |
+
recommendation: The specialist's output text.
|
| 155 |
+
context: The RAG context strings.
|
| 156 |
+
|
| 157 |
+
Returns:
|
| 158 |
+
Tuple of (passed, feedback_message).
|
| 159 |
+
"""
|
| 160 |
+
context_summary = "\n---\n".join(context)
|
| 161 |
+
|
| 162 |
+
system_prompt = (
|
| 163 |
+
"You are a clinical safety auditor. Verify if a treatment recommendation "
|
| 164 |
+
"is STRICTLY grounded in the provided clinical guidelines context.\n\n"
|
| 165 |
+
"Check for:\n"
|
| 166 |
+
"1. Any drug, treatment, or procedure mentioned that is NOT in the context.\n"
|
| 167 |
+
"2. Any dosage or protocol that contradicts the context.\n"
|
| 168 |
+
"3. Any claim presented as fact that lacks support in the context.\n\n"
|
| 169 |
+
"Output a JSON object with two keys:\n"
|
| 170 |
+
'- "verdict": "PASS" or "FAIL"\n'
|
| 171 |
+
'- "issues": a list of specific issues found (empty list if PASS)\n\n'
|
| 172 |
+
"Output ONLY the JSON, nothing else."
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
user_prompt = (
|
| 176 |
+
f"Context:\n{context_summary}\n\n"
|
| 177 |
+
f"Recommendation:\n{recommendation}\n\n"
|
| 178 |
+
"Evaluate the recommendation against the context:"
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
try:
|
| 182 |
+
response = call_tier_model(
|
| 183 |
+
tier=1,
|
| 184 |
+
system_prompt=system_prompt,
|
| 185 |
+
user_prompt=user_prompt,
|
| 186 |
+
max_tokens=200,
|
| 187 |
+
temperature=0.0,
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
response_upper = response.upper()
|
| 191 |
+
if "FAIL" in response_upper:
|
| 192 |
+
feedback = f"ENTAILMENT: {response}"
|
| 193 |
+
return False, feedback
|
| 194 |
+
|
| 195 |
+
return True, ""
|
| 196 |
+
|
| 197 |
+
except Exception as exc:
|
| 198 |
+
logger.warning("Entailment check failed: %s — defaulting to PASS.", exc)
|
| 199 |
+
return True, "" # Fail open if entailment check itself fails
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# ---------------------------------------------------------------------------
|
| 203 |
+
# Critic Node
|
| 204 |
+
# ---------------------------------------------------------------------------
|
| 205 |
+
|
| 206 |
+
def critic_node(state: AgentState) -> Dict[str, Any]:
|
| 207 |
+
"""Validate the specialist's recommendation for safety and completeness.
|
| 208 |
+
|
| 209 |
+
Runs four layers of checks:
|
| 210 |
+
1. Formatting (deterministic — flexible concept matching)
|
| 211 |
+
2. Safety phrases (deterministic — dosage citation check)
|
| 212 |
+
3. Diagnostic rigor (deterministic — no treatment without pathology)
|
| 213 |
+
4. Entailment (LLM-based — only if layers 1-3 pass)
|
| 214 |
+
|
| 215 |
+
If any check fails, returns FAIL with specific feedback.
|
| 216 |
+
The graph will loop back to the specialist if attempts < max.
|
| 217 |
+
|
| 218 |
+
Args:
|
| 219 |
+
state: Current LangGraph state.
|
| 220 |
+
|
| 221 |
+
Returns:
|
| 222 |
+
State update with critic_verdict, critic_feedback, critic_attempts.
|
| 223 |
+
"""
|
| 224 |
+
recommendation = state.get("clinical_recommendation", "")
|
| 225 |
+
context = state.get("rag_context", [])
|
| 226 |
+
current_attempts = state.get("critic_attempts", 0)
|
| 227 |
+
|
| 228 |
+
# Track this attempt
|
| 229 |
+
new_attempts = current_attempts + 1
|
| 230 |
+
|
| 231 |
+
# Guard: empty recommendation
|
| 232 |
+
if not recommendation or not recommendation.strip():
|
| 233 |
+
logger.warning("Critic received empty recommendation — auto-FAIL.")
|
| 234 |
+
return {
|
| 235 |
+
"critic_verdict": "FAIL",
|
| 236 |
+
"critic_feedback": "SYSTEM: Specialist returned empty recommendation.",
|
| 237 |
+
"critic_attempts": new_attempts,
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
# Guard: recommendation is already the safe fallback phrase
|
| 241 |
+
if "información no concluyente" in recommendation.lower():
|
| 242 |
+
return {
|
| 243 |
+
"critic_verdict": "PASS",
|
| 244 |
+
"critic_feedback": "",
|
| 245 |
+
"critic_attempts": new_attempts,
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
# Guard: inference error
|
| 249 |
+
if "error en el sistema de inferencia" in recommendation.lower():
|
| 250 |
+
return {
|
| 251 |
+
"critic_verdict": "FAIL",
|
| 252 |
+
"critic_feedback": "SYSTEM: Inference engine error — cannot validate.",
|
| 253 |
+
"critic_attempts": new_attempts,
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
# --- Layer 1: Formatting check ---
|
| 257 |
+
fmt_pass, fmt_feedback = _check_formatting(recommendation)
|
| 258 |
+
|
| 259 |
+
# --- Layer 2: Safety check ---
|
| 260 |
+
safety_pass, safety_feedback = _check_safety_phrases(recommendation)
|
| 261 |
+
|
| 262 |
+
# --- Layer 3: Diagnostic Rigor check ---
|
| 263 |
+
clinical_text = state.get("clinical_text", "")
|
| 264 |
+
rigor_pass, rigor_feedback = _check_diagnostic_rigor(recommendation, clinical_text)
|
| 265 |
+
|
| 266 |
+
# --- Layer 4: Entailment check (only if layers 1-3 pass) ---
|
| 267 |
+
entailment_pass = True
|
| 268 |
+
entailment_feedback = ""
|
| 269 |
+
if fmt_pass and safety_pass and rigor_pass and context:
|
| 270 |
+
entailment_pass, entailment_feedback = _check_entailment(
|
| 271 |
+
recommendation, context
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
# --- Aggregate verdict ---
|
| 275 |
+
all_passed = fmt_pass and safety_pass and rigor_pass and entailment_pass
|
| 276 |
+
feedbacks = [f for f in [fmt_feedback, safety_feedback, rigor_feedback, entailment_feedback] if f]
|
| 277 |
+
|
| 278 |
+
verdict = "PASS" if all_passed else "FAIL"
|
| 279 |
+
combined_feedback = "\n".join(feedbacks)
|
| 280 |
+
|
| 281 |
+
logger.info(
|
| 282 |
+
"Critic verdict: %s (attempt %d/%d). Issues: %d",
|
| 283 |
+
verdict, new_attempts, MAX_CRITIC_ATTEMPTS, len(feedbacks),
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
return {
|
| 287 |
+
"critic_verdict": verdict,
|
| 288 |
+
"critic_feedback": combined_feedback,
|
| 289 |
+
"critic_attempts": new_attempts,
|
| 290 |
+
}
|
agents/formatter.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Formatter & Fallback Nodes — Structured output and safe degradation.
|
| 3 |
+
|
| 4 |
+
Formatter: transforms the validated recommendation into a structured
|
| 5 |
+
format optimised for the Gradio UI, including confidence reports and
|
| 6 |
+
source citations.
|
| 7 |
+
|
| 8 |
+
Fallback: safe degradation when RAG or reasoning fails, following the
|
| 9 |
+
Anti-Hallucination Policy (Rule #39).
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import logging
|
| 13 |
+
from datetime import datetime, timezone
|
| 14 |
+
from typing import Dict, Any, List
|
| 15 |
+
|
| 16 |
+
from .state import AgentState
|
| 17 |
+
from .tools import get_tier_spec
|
| 18 |
+
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# ---------------------------------------------------------------------------
|
| 23 |
+
# Response Formatter Node
|
| 24 |
+
# ---------------------------------------------------------------------------
|
| 25 |
+
|
| 26 |
+
def formatter_node(state: AgentState) -> Dict[str, Any]:
|
| 27 |
+
"""Transform the validated recommendation into structured UI output.
|
| 28 |
+
|
| 29 |
+
Produces:
|
| 30 |
+
- formatted_recommendation: Markdown with metadata header
|
| 31 |
+
- confidence_report: Dict of all quality metrics
|
| 32 |
+
- source_citations: Formatted bibliography
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
state: Current LangGraph state.
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
State update with formatted output, confidence report, and citations.
|
| 39 |
+
"""
|
| 40 |
+
recommendation = state.get("clinical_recommendation", "")
|
| 41 |
+
tier = state.get("selected_tier", 1)
|
| 42 |
+
spec = get_tier_spec(tier)
|
| 43 |
+
rag_confidence = state.get("rag_confidence", 0.0)
|
| 44 |
+
critic_attempts = state.get("critic_attempts", 0)
|
| 45 |
+
complexity_score = state.get("complexity_score", 0.0)
|
| 46 |
+
rag_sources = state.get("rag_sources", [])
|
| 47 |
+
rag_count = state.get("rag_retrieval_count", 0)
|
| 48 |
+
rag_graded = state.get("rag_grading_pass_count", 0)
|
| 49 |
+
rag_rewrites = state.get("rag_query_rewrites", 0)
|
| 50 |
+
api_evidence = state.get("api_evidence_context", [])
|
| 51 |
+
entities = state.get("extracted_entities", {})
|
| 52 |
+
|
| 53 |
+
# --- Confidence report ---
|
| 54 |
+
confidence_report: Dict[str, Any] = {
|
| 55 |
+
"tier_used": tier,
|
| 56 |
+
"tier_name": spec.name,
|
| 57 |
+
"model_id": spec.model_id,
|
| 58 |
+
"complexity_score": complexity_score,
|
| 59 |
+
"rag_confidence": rag_confidence,
|
| 60 |
+
"rag_retrieval_count": rag_count,
|
| 61 |
+
"rag_graded_relevant": rag_graded,
|
| 62 |
+
"rag_query_rewrites": rag_rewrites,
|
| 63 |
+
"critic_iterations": critic_attempts,
|
| 64 |
+
"api_evidence_count": len(api_evidence),
|
| 65 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
# --- Confidence level label ---
|
| 69 |
+
if rag_confidence >= 0.7:
|
| 70 |
+
confidence_label = "🟢 Alta"
|
| 71 |
+
elif rag_confidence >= 0.4:
|
| 72 |
+
confidence_label = "🟡 Media"
|
| 73 |
+
else:
|
| 74 |
+
confidence_label = "🔴 Baja"
|
| 75 |
+
|
| 76 |
+
# --- Formatted recommendation with metadata header ---
|
| 77 |
+
header = (
|
| 78 |
+
f"---\n"
|
| 79 |
+
f"**OncoAgent — Recomendación Clínica**\n"
|
| 80 |
+
f"📊 Modelo: {spec.name} (Tier {tier}) | "
|
| 81 |
+
f"Confianza RAG: {confidence_label} ({rag_confidence:.2f}) | "
|
| 82 |
+
f"Iteraciones Críticas: {critic_attempts}\n"
|
| 83 |
+
f"🧬 Tipo: {entities.get('cancer_type', 'N/A')} | "
|
| 84 |
+
f"Estadío: {entities.get('stage', 'N/A')} | "
|
| 85 |
+
f"Mutaciones: {', '.join(entities.get('mutations', [])) or 'N/A'}\n"
|
| 86 |
+
f"---\n\n"
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
formatted = header + recommendation
|
| 90 |
+
|
| 91 |
+
# --- Source citations ---
|
| 92 |
+
citations = []
|
| 93 |
+
if rag_sources:
|
| 94 |
+
citations.append("### Fuentes Clínicas (RAG)")
|
| 95 |
+
citations.extend(rag_sources)
|
| 96 |
+
|
| 97 |
+
if api_evidence:
|
| 98 |
+
citations.append("\n### Evidencia Adicional (APIs)")
|
| 99 |
+
citations.extend([f"- {e}" for e in api_evidence])
|
| 100 |
+
|
| 101 |
+
# --- Safety status ---
|
| 102 |
+
safety_status = "Validated against clinical oncology guidelines"
|
| 103 |
+
|
| 104 |
+
return {
|
| 105 |
+
"formatted_recommendation": formatted,
|
| 106 |
+
"confidence_report": confidence_report,
|
| 107 |
+
"source_citations": citations,
|
| 108 |
+
"safety_status": safety_status,
|
| 109 |
+
"is_safe": True,
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# ---------------------------------------------------------------------------
|
| 114 |
+
# Fallback Node (Safe Degradation)
|
| 115 |
+
# ---------------------------------------------------------------------------
|
| 116 |
+
|
| 117 |
+
_SAFE_MESSAGE = (
|
| 118 |
+
"---\n"
|
| 119 |
+
"**OncoAgent — Resultado No Concluyente**\n"
|
| 120 |
+
"---\n\n"
|
| 121 |
+
"## ⚠️ Información no concluyente en las guías provistas.\n\n"
|
| 122 |
+
"El sistema no pudo generar una recomendación clínica confiable "
|
| 123 |
+
"para este caso por una de las siguientes razones:\n\n"
|
| 124 |
+
"1. No se encontró evidencia suficiente en las guías clínicas cargadas.\n"
|
| 125 |
+
"2. La recomendación generada no pasó la validación de seguridad.\n"
|
| 126 |
+
"3. El caso requiere revisión clínica especializada fuera del alcance "
|
| 127 |
+
"de las guías disponibles.\n\n"
|
| 128 |
+
"**Acción recomendada:** Consulte con un oncólogo especialista para "
|
| 129 |
+
"una evaluación personalizada.\n"
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def fallback_node(state: AgentState) -> Dict[str, Any]:
|
| 134 |
+
"""Generate a safe fallback response when the pipeline cannot produce
|
| 135 |
+
a reliable recommendation.
|
| 136 |
+
|
| 137 |
+
This node is triggered when:
|
| 138 |
+
- RAG retrieval yields insufficient relevant documents
|
| 139 |
+
- The critic fails after max iterations
|
| 140 |
+
- The input is too short or unintelligible
|
| 141 |
+
|
| 142 |
+
Args:
|
| 143 |
+
state: Current LangGraph state.
|
| 144 |
+
|
| 145 |
+
Returns:
|
| 146 |
+
State update with safe fallback response and diagnostic info.
|
| 147 |
+
"""
|
| 148 |
+
# Determine why we fell back
|
| 149 |
+
routing = state.get("routing_decision", "")
|
| 150 |
+
rag_count = state.get("rag_retrieval_count", 0)
|
| 151 |
+
critic_verdict = state.get("critic_verdict", "")
|
| 152 |
+
critic_attempts = state.get("critic_attempts", 0)
|
| 153 |
+
|
| 154 |
+
reasons = []
|
| 155 |
+
if routing == "insufficient":
|
| 156 |
+
reasons.append("Input too short or unintelligible for clinical triage.")
|
| 157 |
+
if rag_count == 0:
|
| 158 |
+
reasons.append("No relevant documents found in clinical guidelines database.")
|
| 159 |
+
if critic_verdict == "FAIL" and critic_attempts >= 2:
|
| 160 |
+
reasons.append(
|
| 161 |
+
f"Recommendation failed safety validation after {critic_attempts} attempts."
|
| 162 |
+
)
|
| 163 |
+
if not reasons:
|
| 164 |
+
reasons.append("Unknown system error — safe fallback triggered.")
|
| 165 |
+
|
| 166 |
+
fallback_reason = " | ".join(reasons)
|
| 167 |
+
logger.warning("Fallback triggered: %s", fallback_reason)
|
| 168 |
+
|
| 169 |
+
return {
|
| 170 |
+
"formatted_recommendation": _SAFE_MESSAGE,
|
| 171 |
+
"clinical_recommendation": "Información no concluyente en las guías provistas.",
|
| 172 |
+
"confidence_report": {
|
| 173 |
+
"tier_used": state.get("selected_tier", 0),
|
| 174 |
+
"fallback": True,
|
| 175 |
+
"reason": fallback_reason,
|
| 176 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
| 177 |
+
},
|
| 178 |
+
"source_citations": [],
|
| 179 |
+
"fallback_reason": fallback_reason,
|
| 180 |
+
"safety_status": f"Fallback: {fallback_reason}",
|
| 181 |
+
"is_safe": False,
|
| 182 |
+
}
|
agents/graph.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
OncoAgent LangGraph — SOTA Multi-Agent Orchestration Graph.
|
| 3 |
+
|
| 4 |
+
Architecture synthesised from:
|
| 5 |
+
- Claude Code: deterministic harness + sub-agent delegation
|
| 6 |
+
- Hermes Agent: structured tool calling + persistent state
|
| 7 |
+
- Corrective RAG: graded retrieval with query rewriting
|
| 8 |
+
- Reflexion: generator ↔ critic loop with max iterations
|
| 9 |
+
- Model Tiering: Qwen3.5-9B (fast) ↔ Qwen3.6-27B (deep reasoning)
|
| 10 |
+
|
| 11 |
+
Topology:
|
| 12 |
+
Router → Ingestion → Corrective RAG → Specialist ↔ Critic → HITL Gate → Formatter
|
| 13 |
+
↓
|
| 14 |
+
Fallback
|
| 15 |
+
|
| 16 |
+
Conditional edges:
|
| 17 |
+
- Router: routes "insufficient" directly to fallback
|
| 18 |
+
- CRAG: routes insufficient docs to fallback
|
| 19 |
+
- Critic: loops back to specialist (max 2) or to fallback
|
| 20 |
+
- HITL: routes high-acuity to interrupt, others to formatter
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
import logging
|
| 24 |
+
from langgraph.graph import StateGraph, END
|
| 25 |
+
from langgraph.checkpoint.memory import MemorySaver
|
| 26 |
+
|
| 27 |
+
from .state import AgentState
|
| 28 |
+
from .router import router_node
|
| 29 |
+
from .nodes import data_ingestion_node
|
| 30 |
+
from .corrective_rag import corrective_rag_node
|
| 31 |
+
from .specialist import specialist_node
|
| 32 |
+
from .critic import critic_node, MAX_CRITIC_ATTEMPTS
|
| 33 |
+
from .formatter import formatter_node, fallback_node
|
| 34 |
+
|
| 35 |
+
logger = logging.getLogger(__name__)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# ---------------------------------------------------------------------------
|
| 39 |
+
# Conditional edge functions
|
| 40 |
+
# ---------------------------------------------------------------------------
|
| 41 |
+
|
| 42 |
+
def _route_after_router(state: AgentState) -> str:
|
| 43 |
+
"""Route based on the router's complexity classification.
|
| 44 |
+
|
| 45 |
+
Returns:
|
| 46 |
+
Node name to transition to.
|
| 47 |
+
"""
|
| 48 |
+
decision = state.get("routing_decision", "simple")
|
| 49 |
+
if decision == "insufficient":
|
| 50 |
+
logger.info("Router → Fallback (insufficient input)")
|
| 51 |
+
return "fallback"
|
| 52 |
+
# Both "simple" and "complex" proceed to ingestion
|
| 53 |
+
return "ingestion"
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _route_after_crag(state: AgentState) -> str:
|
| 57 |
+
"""Route based on CRAG retrieval results.
|
| 58 |
+
|
| 59 |
+
If insufficient relevant documents were found (even after rewrites),
|
| 60 |
+
route directly to fallback.
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
Node name to transition to.
|
| 64 |
+
"""
|
| 65 |
+
graded_count = state.get("rag_grading_pass_count", 0)
|
| 66 |
+
retrieval_count = state.get("rag_retrieval_count", 0)
|
| 67 |
+
|
| 68 |
+
if retrieval_count == 0 and graded_count == 0:
|
| 69 |
+
logger.info("CRAG → Fallback (no relevant documents)")
|
| 70 |
+
return "fallback"
|
| 71 |
+
|
| 72 |
+
return "specialist"
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _route_after_critic(state: AgentState) -> str:
|
| 76 |
+
"""Route based on the critic's verdict and attempt count.
|
| 77 |
+
|
| 78 |
+
- PASS → proceed to HITL gate
|
| 79 |
+
- FAIL + attempts < max → loop back to specialist
|
| 80 |
+
- FAIL + attempts >= max → fallback
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
Node name to transition to.
|
| 84 |
+
"""
|
| 85 |
+
verdict = state.get("critic_verdict", "FAIL")
|
| 86 |
+
attempts = state.get("critic_attempts", 0)
|
| 87 |
+
|
| 88 |
+
if verdict == "PASS":
|
| 89 |
+
logger.info("Critic → HITL Gate (PASS on attempt %d)", attempts)
|
| 90 |
+
return "hitl_gate"
|
| 91 |
+
|
| 92 |
+
if attempts >= MAX_CRITIC_ATTEMPTS:
|
| 93 |
+
logger.warning(
|
| 94 |
+
"Critic → Fallback (FAIL after %d/%d attempts)",
|
| 95 |
+
attempts, MAX_CRITIC_ATTEMPTS,
|
| 96 |
+
)
|
| 97 |
+
return "fallback"
|
| 98 |
+
|
| 99 |
+
logger.info(
|
| 100 |
+
"Critic → Specialist retry (FAIL, attempt %d/%d)",
|
| 101 |
+
attempts, MAX_CRITIC_ATTEMPTS,
|
| 102 |
+
)
|
| 103 |
+
return "specialist"
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _route_after_hitl(state: AgentState) -> str:
|
| 107 |
+
"""Route based on acuity level and HITL requirements.
|
| 108 |
+
|
| 109 |
+
For the hackathon, high-acuity cases are flagged but auto-proceed.
|
| 110 |
+
In production, this would use LangGraph's interrupt() for real
|
| 111 |
+
clinician approval.
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
Node name to transition to.
|
| 115 |
+
"""
|
| 116 |
+
# For now, always proceed to formatter
|
| 117 |
+
# In production: if hitl_required and not hitl_approved → interrupt
|
| 118 |
+
return "formatter"
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# ---------------------------------------------------------------------------
|
| 122 |
+
# HITL Gate Node
|
| 123 |
+
# ---------------------------------------------------------------------------
|
| 124 |
+
|
| 125 |
+
def hitl_gate_node(state: AgentState) -> dict:
|
| 126 |
+
"""Determine if the case requires Human-in-the-Loop approval.
|
| 127 |
+
|
| 128 |
+
Acuity classification:
|
| 129 |
+
- high: Stage IV + rare mutations → requires clinician review
|
| 130 |
+
- medium: Stage III or complex → flagged but auto-proceeds
|
| 131 |
+
- low: Standard cases → auto-proceeds
|
| 132 |
+
|
| 133 |
+
Args:
|
| 134 |
+
state: Current LangGraph state.
|
| 135 |
+
|
| 136 |
+
Returns:
|
| 137 |
+
State update with acuity_level, hitl_required, hitl_approved.
|
| 138 |
+
"""
|
| 139 |
+
entities = state.get("extracted_entities", {})
|
| 140 |
+
complexity = state.get("complexity_score", 0.0)
|
| 141 |
+
stage = entities.get("stage", "Unknown").upper()
|
| 142 |
+
|
| 143 |
+
# Determine acuity
|
| 144 |
+
if "IV" in stage and complexity >= 0.6:
|
| 145 |
+
acuity = "high"
|
| 146 |
+
hitl_required = True
|
| 147 |
+
elif "III" in stage or complexity >= 0.4:
|
| 148 |
+
acuity = "medium"
|
| 149 |
+
hitl_required = False
|
| 150 |
+
else:
|
| 151 |
+
acuity = "low"
|
| 152 |
+
hitl_required = False
|
| 153 |
+
|
| 154 |
+
logger.info(
|
| 155 |
+
"HITL Gate: acuity=%s, hitl_required=%s, complexity=%.2f",
|
| 156 |
+
acuity, hitl_required, complexity,
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
return {
|
| 160 |
+
"acuity_level": acuity,
|
| 161 |
+
"hitl_required": hitl_required,
|
| 162 |
+
"hitl_approved": not hitl_required, # Auto-approve non-HITL cases
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
# ---------------------------------------------------------------------------
|
| 167 |
+
# Graph Builder
|
| 168 |
+
# ---------------------------------------------------------------------------
|
| 169 |
+
|
| 170 |
+
def build_oncoagent_graph() -> StateGraph:
|
| 171 |
+
"""Build the SOTA OncoAgent LangGraph state machine.
|
| 172 |
+
|
| 173 |
+
Topology:
|
| 174 |
+
START → router → (ingestion | fallback)
|
| 175 |
+
↓
|
| 176 |
+
corrective_rag → (specialist | fallback)
|
| 177 |
+
↓
|
| 178 |
+
specialist ↔ critic (max 2 loops)
|
| 179 |
+
↓
|
| 180 |
+
hitl_gate → formatter → END
|
| 181 |
+
↓
|
| 182 |
+
fallback → END
|
| 183 |
+
|
| 184 |
+
Returns:
|
| 185 |
+
Compiled LangGraph state machine.
|
| 186 |
+
"""
|
| 187 |
+
workflow = StateGraph(AgentState)
|
| 188 |
+
|
| 189 |
+
# --- Define Nodes ---
|
| 190 |
+
workflow.add_node("router", router_node)
|
| 191 |
+
workflow.add_node("ingestion", data_ingestion_node)
|
| 192 |
+
workflow.add_node("corrective_rag", corrective_rag_node)
|
| 193 |
+
workflow.add_node("specialist", specialist_node)
|
| 194 |
+
workflow.add_node("critic", critic_node)
|
| 195 |
+
workflow.add_node("hitl_gate", hitl_gate_node)
|
| 196 |
+
workflow.add_node("formatter", formatter_node)
|
| 197 |
+
workflow.add_node("fallback", fallback_node)
|
| 198 |
+
|
| 199 |
+
# --- Define Edges ---
|
| 200 |
+
# Entry point
|
| 201 |
+
workflow.set_entry_point("router")
|
| 202 |
+
|
| 203 |
+
# Router → Ingestion or Fallback (conditional)
|
| 204 |
+
workflow.add_conditional_edges(
|
| 205 |
+
"router",
|
| 206 |
+
_route_after_router,
|
| 207 |
+
{
|
| 208 |
+
"ingestion": "ingestion",
|
| 209 |
+
"fallback": "fallback",
|
| 210 |
+
},
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
# Ingestion → Corrective RAG (always)
|
| 214 |
+
workflow.add_edge("ingestion", "corrective_rag")
|
| 215 |
+
|
| 216 |
+
# Corrective RAG → Specialist or Fallback (conditional)
|
| 217 |
+
workflow.add_conditional_edges(
|
| 218 |
+
"corrective_rag",
|
| 219 |
+
_route_after_crag,
|
| 220 |
+
{
|
| 221 |
+
"specialist": "specialist",
|
| 222 |
+
"fallback": "fallback",
|
| 223 |
+
},
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
# Specialist → Critic (always)
|
| 227 |
+
workflow.add_edge("specialist", "critic")
|
| 228 |
+
|
| 229 |
+
# Critic → HITL Gate, Specialist (retry), or Fallback (conditional)
|
| 230 |
+
workflow.add_conditional_edges(
|
| 231 |
+
"critic",
|
| 232 |
+
_route_after_critic,
|
| 233 |
+
{
|
| 234 |
+
"hitl_gate": "hitl_gate",
|
| 235 |
+
"specialist": "specialist",
|
| 236 |
+
"fallback": "fallback",
|
| 237 |
+
},
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
# HITL Gate → Formatter (conditional, future: interrupt for clinician)
|
| 241 |
+
workflow.add_conditional_edges(
|
| 242 |
+
"hitl_gate",
|
| 243 |
+
_route_after_hitl,
|
| 244 |
+
{
|
| 245 |
+
"formatter": "formatter",
|
| 246 |
+
},
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
# Terminal edges
|
| 250 |
+
workflow.add_edge("formatter", END)
|
| 251 |
+
workflow.add_edge("fallback", END)
|
| 252 |
+
|
| 253 |
+
# Compile with recursion limit (Rule #20: strict limit for loops)
|
| 254 |
+
memory = MemorySaver()
|
| 255 |
+
compiled = workflow.compile(
|
| 256 |
+
checkpointer=memory,
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
logger.info("OncoAgent graph compiled successfully (8 nodes, SOTA topology).")
|
| 260 |
+
return compiled
|
agents/memory.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Per-patient session memory for OncoAgent.
|
| 3 |
+
|
| 4 |
+
Design inspired by Hermes Agent's persistent memory:
|
| 5 |
+
- Each patient gets an isolated profile with their own clinical history.
|
| 6 |
+
- Memory is scoped per ``patient_id``, never global.
|
| 7 |
+
- Thread-safe via a simple dict-based store (swap for Redis/SQLite
|
| 8 |
+
in production if needed).
|
| 9 |
+
|
| 10 |
+
Usage:
|
| 11 |
+
store = PatientMemoryStore()
|
| 12 |
+
store.save_interaction(patient_id="P001", interaction={...})
|
| 13 |
+
history = store.get_history(patient_id="P001")
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import logging
|
| 17 |
+
import uuid
|
| 18 |
+
from datetime import datetime, timezone
|
| 19 |
+
from typing import Dict, Any, List, Optional
|
| 20 |
+
from dataclasses import dataclass, field
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@dataclass
|
| 26 |
+
class PatientProfile:
|
| 27 |
+
"""Isolated memory profile for a single patient.
|
| 28 |
+
|
| 29 |
+
Attributes:
|
| 30 |
+
patient_id: Unique identifier for the patient.
|
| 31 |
+
created_at: ISO timestamp of profile creation.
|
| 32 |
+
interactions: Ordered list of past query/response pairs.
|
| 33 |
+
metadata: Arbitrary metadata (e.g., preferred language).
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
patient_id: str
|
| 37 |
+
created_at: str = field(
|
| 38 |
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
| 39 |
+
)
|
| 40 |
+
interactions: List[Dict[str, Any]] = field(default_factory=list)
|
| 41 |
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
| 42 |
+
|
| 43 |
+
def add_interaction(self, interaction: Dict[str, Any]) -> None:
|
| 44 |
+
"""Append an interaction to the patient's history.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
interaction: Dict with at minimum ``query`` and ``response`` keys.
|
| 48 |
+
"""
|
| 49 |
+
interaction["timestamp"] = datetime.now(timezone.utc).isoformat()
|
| 50 |
+
interaction["interaction_id"] = str(uuid.uuid4())[:8]
|
| 51 |
+
self.interactions.append(interaction)
|
| 52 |
+
logger.debug(
|
| 53 |
+
"Patient %s: stored interaction #%d",
|
| 54 |
+
self.patient_id,
|
| 55 |
+
len(self.interactions),
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
def get_recent_context(self, n: int = 3) -> List[Dict[str, Any]]:
|
| 59 |
+
"""Return the last *n* interactions for context injection.
|
| 60 |
+
|
| 61 |
+
Args:
|
| 62 |
+
n: Number of recent interactions to return.
|
| 63 |
+
|
| 64 |
+
Returns:
|
| 65 |
+
List of the most recent interactions (newest last).
|
| 66 |
+
"""
|
| 67 |
+
return self.interactions[-n:]
|
| 68 |
+
|
| 69 |
+
def summary(self) -> str:
|
| 70 |
+
"""Return a brief summary string for logging/UI display."""
|
| 71 |
+
return (
|
| 72 |
+
f"Patient {self.patient_id} | "
|
| 73 |
+
f"{len(self.interactions)} interactions | "
|
| 74 |
+
f"Created: {self.created_at}"
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class PatientMemoryStore:
|
| 79 |
+
"""In-memory store for per-patient profiles.
|
| 80 |
+
|
| 81 |
+
For hackathon scope this uses a simple dict. In production,
|
| 82 |
+
replace with SQLite / Redis for persistence across restarts.
|
| 83 |
+
"""
|
| 84 |
+
|
| 85 |
+
def __init__(self) -> None:
|
| 86 |
+
self._profiles: Dict[str, PatientProfile] = {}
|
| 87 |
+
|
| 88 |
+
def get_or_create_profile(
|
| 89 |
+
self,
|
| 90 |
+
patient_id: Optional[str] = None,
|
| 91 |
+
) -> PatientProfile:
|
| 92 |
+
"""Retrieve an existing profile or create a new one.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
patient_id: Existing patient ID. If None, generates a new one.
|
| 96 |
+
|
| 97 |
+
Returns:
|
| 98 |
+
The corresponding PatientProfile.
|
| 99 |
+
"""
|
| 100 |
+
if patient_id is None:
|
| 101 |
+
patient_id = f"P-{str(uuid.uuid4())[:8].upper()}"
|
| 102 |
+
|
| 103 |
+
if patient_id not in self._profiles:
|
| 104 |
+
self._profiles[patient_id] = PatientProfile(patient_id=patient_id)
|
| 105 |
+
logger.info("Created new patient profile: %s", patient_id)
|
| 106 |
+
|
| 107 |
+
return self._profiles[patient_id]
|
| 108 |
+
|
| 109 |
+
def save_interaction(
|
| 110 |
+
self,
|
| 111 |
+
patient_id: str,
|
| 112 |
+
interaction: Dict[str, Any],
|
| 113 |
+
) -> None:
|
| 114 |
+
"""Save an interaction to a patient's profile.
|
| 115 |
+
|
| 116 |
+
Args:
|
| 117 |
+
patient_id: Target patient ID.
|
| 118 |
+
interaction: Dict with query/response data.
|
| 119 |
+
"""
|
| 120 |
+
profile = self.get_or_create_profile(patient_id)
|
| 121 |
+
profile.add_interaction(interaction)
|
| 122 |
+
|
| 123 |
+
def get_history(
|
| 124 |
+
self,
|
| 125 |
+
patient_id: str,
|
| 126 |
+
n: Optional[int] = None,
|
| 127 |
+
) -> List[Dict[str, Any]]:
|
| 128 |
+
"""Retrieve a patient's interaction history.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
patient_id: Target patient ID.
|
| 132 |
+
n: If provided, return only the last *n* interactions.
|
| 133 |
+
|
| 134 |
+
Returns:
|
| 135 |
+
List of interaction dicts.
|
| 136 |
+
"""
|
| 137 |
+
profile = self._profiles.get(patient_id)
|
| 138 |
+
if profile is None:
|
| 139 |
+
return []
|
| 140 |
+
if n is not None:
|
| 141 |
+
return profile.get_recent_context(n)
|
| 142 |
+
return profile.interactions
|
| 143 |
+
|
| 144 |
+
def list_patients(self) -> List[str]:
|
| 145 |
+
"""Return all known patient IDs."""
|
| 146 |
+
return list(self._profiles.keys())
|
| 147 |
+
|
| 148 |
+
def patient_count(self) -> int:
|
| 149 |
+
"""Return the number of tracked patients."""
|
| 150 |
+
return len(self._profiles)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
# Module-level singleton
|
| 154 |
+
_global_memory_store: Optional[PatientMemoryStore] = None
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def get_memory_store() -> PatientMemoryStore:
|
| 158 |
+
"""Return the global PatientMemoryStore singleton."""
|
| 159 |
+
global _global_memory_store
|
| 160 |
+
if _global_memory_store is None:
|
| 161 |
+
_global_memory_store = PatientMemoryStore()
|
| 162 |
+
return _global_memory_store
|
agents/nodes.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LangGraph Node implementations for OncoAgent.
|
| 3 |
+
|
| 4 |
+
This module retains the data ingestion node (PHI cleaning + entity extraction)
|
| 5 |
+
and re-exports all other nodes from their dedicated modules for backward
|
| 6 |
+
compatibility.
|
| 7 |
+
|
| 8 |
+
Module organisation (SOTA redesign):
|
| 9 |
+
- agents/router.py → Router Node (complexity classification)
|
| 10 |
+
- agents/corrective_rag.py → Corrective RAG Node (graded retrieval)
|
| 11 |
+
- agents/specialist.py → Specialist Node (tier-adaptive reasoning)
|
| 12 |
+
- agents/critic.py → Critic Node (reflexion validation)
|
| 13 |
+
- agents/formatter.py → Formatter + Fallback Nodes
|
| 14 |
+
- agents/tools.py → Shared vLLM client + tier calling
|
| 15 |
+
- agents/memory.py → Per-patient session memory
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from typing import Dict, Any
|
| 19 |
+
|
| 20 |
+
import re
|
| 21 |
+
import logging
|
| 22 |
+
|
| 23 |
+
from .state import AgentState
|
| 24 |
+
|
| 25 |
+
logger = logging.getLogger(__name__)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# ---------------------------------------------------------------------------
|
| 29 |
+
# PHI Patterns (Zero-PHI Policy — Rule #39)
|
| 30 |
+
# ---------------------------------------------------------------------------
|
| 31 |
+
|
| 32 |
+
_PHI_PATTERNS = [
|
| 33 |
+
re.compile(r"\b\d{3}-\d{2}-\d{4}\b"), # SSN
|
| 34 |
+
re.compile(r"\b\d{2}/\d{2}/\d{4}\b"), # Date of birth
|
| 35 |
+
re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}"), # Email
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# ---------------------------------------------------------------------------
|
| 40 |
+
# Node 1: Data Ingestion — PHI cleaning & entity extraction
|
| 41 |
+
# ---------------------------------------------------------------------------
|
| 42 |
+
|
| 43 |
+
def data_ingestion_node(state: AgentState) -> Dict[str, Any]:
|
| 44 |
+
"""Clean the input clinical text (Zero-PHI policy) and extract
|
| 45 |
+
key medical entities via rule-based heuristics.
|
| 46 |
+
|
| 47 |
+
Enhanced extraction includes:
|
| 48 |
+
- Cancer type identification (20+ types)
|
| 49 |
+
- TNM staging parsing
|
| 50 |
+
- Biomarker/mutation detection (15+ markers)
|
| 51 |
+
- Performance status detection (ECOG)
|
| 52 |
+
- Urgency signals
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
state: Current LangGraph state with ``clinical_text``.
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
State update with ``extracted_entities`` and ``phi_detected``.
|
| 59 |
+
"""
|
| 60 |
+
text: str = state.get("clinical_text", "")
|
| 61 |
+
|
| 62 |
+
# --- Zero-PHI check and redaction ---
|
| 63 |
+
phi_found = False
|
| 64 |
+
cleaned_text = text
|
| 65 |
+
for pattern in _PHI_PATTERNS:
|
| 66 |
+
if pattern.search(text):
|
| 67 |
+
phi_found = True
|
| 68 |
+
# Redact detected PHI
|
| 69 |
+
cleaned_text = pattern.sub("[REDACTED]", cleaned_text)
|
| 70 |
+
|
| 71 |
+
if phi_found:
|
| 72 |
+
logger.warning("PHI detected and redacted from clinical input.")
|
| 73 |
+
|
| 74 |
+
# Use cleaned text for downstream processing
|
| 75 |
+
text = cleaned_text
|
| 76 |
+
|
| 77 |
+
# --- Rule-based entity extraction ---
|
| 78 |
+
extracted: Dict[str, Any] = {
|
| 79 |
+
"cancer_type": "Unknown",
|
| 80 |
+
"stage": "Unknown",
|
| 81 |
+
"mutations": [],
|
| 82 |
+
"ecog_status": "Unknown",
|
| 83 |
+
"urgency": "routine",
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
text_lower = text.lower()
|
| 87 |
+
|
| 88 |
+
# Cancer type heuristic (Explicit + Symptom-based risk)
|
| 89 |
+
cancer_keywords = {
|
| 90 |
+
"breast": "Breast Cancer",
|
| 91 |
+
"lung": "Lung Cancer",
|
| 92 |
+
"non-small cell": "Non-Small Cell Lung Cancer",
|
| 93 |
+
"small cell lung": "Small Cell Lung Cancer",
|
| 94 |
+
"colon": "Colon Cancer",
|
| 95 |
+
"colorectal": "Colorectal Cancer",
|
| 96 |
+
"prostate": "Prostate Cancer",
|
| 97 |
+
"pancreatic": "Pancreatic Cancer",
|
| 98 |
+
"hepatocellular": "Hepatocellular Carcinoma",
|
| 99 |
+
"hcc": "Hepatocellular Carcinoma",
|
| 100 |
+
"melanoma": "Melanoma",
|
| 101 |
+
"renal": "Renal Cell Carcinoma",
|
| 102 |
+
"bladder": "Bladder Cancer",
|
| 103 |
+
"ovarian": "Ovarian Cancer",
|
| 104 |
+
"cervical": "Cervical Cancer",
|
| 105 |
+
"thyroid": "Thyroid Cancer",
|
| 106 |
+
"leukemia": "Leukemia",
|
| 107 |
+
"lymphoma": "Lymphoma",
|
| 108 |
+
"myeloma": "Multiple Myeloma",
|
| 109 |
+
"sarcoma": "Sarcoma",
|
| 110 |
+
"glioma": "Glioma",
|
| 111 |
+
"glioblastoma": "Glioblastoma",
|
| 112 |
+
"esophageal": "Esophageal Cancer",
|
| 113 |
+
"gastric": "Gastric Cancer",
|
| 114 |
+
"cholangiocarcinoma": "Cholangiocarcinoma",
|
| 115 |
+
"mesothelioma": "Mesothelioma",
|
| 116 |
+
"uterine": "Uterine Cancer",
|
| 117 |
+
"endometrial": "Uterine Cancer",
|
| 118 |
+
# Symptom-based risk mapping (Triage mode) - Multilingual support
|
| 119 |
+
"menstru": "Uterine Cancer",
|
| 120 |
+
"vaginal": "Uterine Cancer",
|
| 121 |
+
"bleeding": "Uterine Cancer",
|
| 122 |
+
"sangrado": "Uterine Cancer",
|
| 123 |
+
"periods": "Uterine Cancer",
|
| 124 |
+
"periodo": "Uterine Cancer",
|
| 125 |
+
"postmenopausal": "Uterine Cancer",
|
| 126 |
+
"postmenopau": "Uterine Cancer",
|
| 127 |
+
"hemorragia": "Uterine Cancer",
|
| 128 |
+
}
|
| 129 |
+
for keyword, label in cancer_keywords.items():
|
| 130 |
+
if keyword in text_lower:
|
| 131 |
+
extracted["cancer_type"] = label
|
| 132 |
+
break
|
| 133 |
+
|
| 134 |
+
# Stage heuristic (supports TNM and simple staging)
|
| 135 |
+
stage_match = re.search(
|
| 136 |
+
r"stage\s+(I{1,3}V?|[1-4]|iv|iii|ii|i)\b",
|
| 137 |
+
text,
|
| 138 |
+
re.IGNORECASE,
|
| 139 |
+
)
|
| 140 |
+
if stage_match:
|
| 141 |
+
extracted["stage"] = f"Stage {stage_match.group(1).upper()}"
|
| 142 |
+
|
| 143 |
+
# TNM staging
|
| 144 |
+
tnm_match = re.search(
|
| 145 |
+
r"\b(T[0-4x]N[0-3x]M[01x])\b",
|
| 146 |
+
text,
|
| 147 |
+
re.IGNORECASE,
|
| 148 |
+
)
|
| 149 |
+
if tnm_match:
|
| 150 |
+
extracted["tnm"] = tnm_match.group(1).upper()
|
| 151 |
+
|
| 152 |
+
# Mutation heuristic (expanded)
|
| 153 |
+
mutations_found = re.findall(
|
| 154 |
+
r"\b(EGFR|ALK|KRAS|BRAF|HER2|TP53|BRCA[12]|PD-?L1|ROS1|MET|RET|"
|
| 155 |
+
r"NTRK|PIK3CA|MSI-?H|dMMR|FGFR[1-4]?|IDH[12]?|ERBB2|CDK[46]|"
|
| 156 |
+
r"PTEN|APC|VEGF|mTOR)\b",
|
| 157 |
+
text,
|
| 158 |
+
re.IGNORECASE,
|
| 159 |
+
)
|
| 160 |
+
if mutations_found:
|
| 161 |
+
extracted["mutations"] = list(set(m.upper() for m in mutations_found))
|
| 162 |
+
|
| 163 |
+
# ECOG Performance Status
|
| 164 |
+
ecog_match = re.search(
|
| 165 |
+
r"(?:ECOG|performance\s+status)\s*(?:of\s*)?(\d)",
|
| 166 |
+
text,
|
| 167 |
+
re.IGNORECASE,
|
| 168 |
+
)
|
| 169 |
+
if ecog_match:
|
| 170 |
+
extracted["ecog_status"] = f"ECOG {ecog_match.group(1)}"
|
| 171 |
+
|
| 172 |
+
# Urgency detection
|
| 173 |
+
urgency_keywords = [
|
| 174 |
+
"urgent", "emergency", "critical", "immediate",
|
| 175 |
+
"rapidly progressing", "acute", "life-threatening",
|
| 176 |
+
]
|
| 177 |
+
for kw in urgency_keywords:
|
| 178 |
+
if kw in text_lower:
|
| 179 |
+
extracted["urgency"] = "urgent"
|
| 180 |
+
break
|
| 181 |
+
|
| 182 |
+
return {
|
| 183 |
+
"clinical_text": cleaned_text,
|
| 184 |
+
"extracted_entities": extracted,
|
| 185 |
+
"phi_detected": phi_found,
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
# ---------------------------------------------------------------------------
|
| 190 |
+
# Re-exports for backward compatibility
|
| 191 |
+
# ---------------------------------------------------------------------------
|
| 192 |
+
|
| 193 |
+
from .corrective_rag import corrective_rag_node as rag_retrieval_node # noqa: E402, F401
|
| 194 |
+
from .specialist import specialist_node as clinical_specialist_node # noqa: E402, F401
|
| 195 |
+
from .critic import critic_node as safety_validator_node # noqa: E402, F401
|
agents/router.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Router Node — Complexity classification and model tier selection.
|
| 3 |
+
|
| 4 |
+
Design pattern: Supervisor Routing (LangGraph SOTA)
|
| 5 |
+
Inspired by:
|
| 6 |
+
- Claude Code: deterministic routing via structured logic, not free text
|
| 7 |
+
- Hermes Agent: structured JSON output for decisions
|
| 8 |
+
|
| 9 |
+
The router classifies each clinical case into one of three categories:
|
| 10 |
+
- ``simple``: Well-known cancer + standard staging → Tier 1 (9B)
|
| 11 |
+
- ``complex``: Rare cancer / multi-mutation / ambiguous staging → Tier 2 (27B)
|
| 12 |
+
- ``insufficient``: Input too short or unintelligible → direct fallback
|
| 13 |
+
|
| 14 |
+
Supports manual tier override from the UI (user can force Tier 1 or 2).
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import logging
|
| 18 |
+
import json
|
| 19 |
+
from typing import Dict, Any, Optional
|
| 20 |
+
|
| 21 |
+
from .state import AgentState
|
| 22 |
+
|
| 23 |
+
logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# ---------------------------------------------------------------------------
|
| 27 |
+
# Complexity heuristics
|
| 28 |
+
# ---------------------------------------------------------------------------
|
| 29 |
+
|
| 30 |
+
# Cancer types considered "well-documented" with standard NCCN guidelines
|
| 31 |
+
_COMMON_CANCERS = frozenset({
|
| 32 |
+
"breast cancer", "lung cancer", "colon cancer", "colorectal cancer",
|
| 33 |
+
"prostate cancer", "melanoma", "bladder cancer", "thyroid cancer",
|
| 34 |
+
"cervical cancer", "ovarian cancer", "gastric cancer",
|
| 35 |
+
})
|
| 36 |
+
|
| 37 |
+
# Cancer types considered rare or requiring deeper reasoning
|
| 38 |
+
_RARE_CANCERS = frozenset({
|
| 39 |
+
"pancreatic cancer", "hepatocellular carcinoma", "sarcoma",
|
| 40 |
+
"glioma", "glioblastoma", "multiple myeloma", "renal cell carcinoma",
|
| 41 |
+
"esophageal cancer", "cholangiocarcinoma", "mesothelioma",
|
| 42 |
+
"neuroendocrine tumor", "adrenocortical carcinoma",
|
| 43 |
+
})
|
| 44 |
+
|
| 45 |
+
# Mutations that indicate multi-pathway complexity
|
| 46 |
+
_COMPLEX_MUTATIONS = frozenset({
|
| 47 |
+
"EGFR", "ALK", "KRAS", "NTRK", "RET", "MET", "ROS1",
|
| 48 |
+
"PIK3CA", "MSI-H", "DMMR", "BRAF V600E",
|
| 49 |
+
})
|
| 50 |
+
|
| 51 |
+
# Minimum character count for a clinically meaningful input
|
| 52 |
+
_MIN_INPUT_LENGTH = 30
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _classify_complexity(
|
| 56 |
+
clinical_text: str,
|
| 57 |
+
entities: Dict[str, Any],
|
| 58 |
+
) -> tuple[str, float, int]:
|
| 59 |
+
"""Classify case complexity using rule-based heuristics.
|
| 60 |
+
|
| 61 |
+
Args:
|
| 62 |
+
clinical_text: Raw clinical text.
|
| 63 |
+
entities: Extracted entities from the ingestion node.
|
| 64 |
+
|
| 65 |
+
Returns:
|
| 66 |
+
Tuple of (routing_decision, complexity_score, recommended_tier).
|
| 67 |
+
"""
|
| 68 |
+
# Gate: insufficient input
|
| 69 |
+
if len(clinical_text.strip()) < _MIN_INPUT_LENGTH:
|
| 70 |
+
logger.info("Input too short (%d chars) — routing to insufficient.", len(clinical_text))
|
| 71 |
+
return "insufficient", 0.0, 1
|
| 72 |
+
|
| 73 |
+
score = 0.0
|
| 74 |
+
cancer_type = entities.get("cancer_type", "Unknown").lower()
|
| 75 |
+
stage = entities.get("stage", "Unknown")
|
| 76 |
+
mutations = entities.get("mutations", [])
|
| 77 |
+
|
| 78 |
+
# --- Cancer type scoring ---
|
| 79 |
+
if cancer_type in _RARE_CANCERS:
|
| 80 |
+
score += 0.4
|
| 81 |
+
elif cancer_type == "unknown":
|
| 82 |
+
score += 0.3 # Unidentified cancer is inherently complex
|
| 83 |
+
# Common cancers add no complexity
|
| 84 |
+
|
| 85 |
+
# --- Stage scoring ---
|
| 86 |
+
if "IV" in stage.upper():
|
| 87 |
+
score += 0.25
|
| 88 |
+
elif "III" in stage.upper():
|
| 89 |
+
score += 0.15
|
| 90 |
+
|
| 91 |
+
# --- Mutation complexity ---
|
| 92 |
+
complex_muts = [m for m in mutations if m.upper() in _COMPLEX_MUTATIONS]
|
| 93 |
+
if len(complex_muts) >= 2:
|
| 94 |
+
score += 0.3 # Multi-mutation = high complexity
|
| 95 |
+
elif len(complex_muts) == 1:
|
| 96 |
+
score += 0.15
|
| 97 |
+
|
| 98 |
+
# --- Prior treatment mentions (heuristic) ---
|
| 99 |
+
prior_treatment_keywords = [
|
| 100 |
+
"prior treatment", "previously treated", "relapsed",
|
| 101 |
+
"refractory", "second-line", "third-line", "progression",
|
| 102 |
+
"resistance", "failed", "recurrent",
|
| 103 |
+
]
|
| 104 |
+
text_lower = clinical_text.lower()
|
| 105 |
+
for kw in prior_treatment_keywords:
|
| 106 |
+
if kw in text_lower:
|
| 107 |
+
score += 0.1
|
| 108 |
+
break
|
| 109 |
+
|
| 110 |
+
# Clamp to [0, 1]
|
| 111 |
+
score = min(score, 1.0)
|
| 112 |
+
|
| 113 |
+
# Decision boundary
|
| 114 |
+
if score >= 0.5:
|
| 115 |
+
return "complex", score, 2
|
| 116 |
+
else:
|
| 117 |
+
return "simple", score, 1
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
# ---------------------------------------------------------------------------
|
| 121 |
+
# Router Node
|
| 122 |
+
# ---------------------------------------------------------------------------
|
| 123 |
+
|
| 124 |
+
def router_node(state: AgentState) -> Dict[str, Any]:
|
| 125 |
+
"""Classify case complexity and select the appropriate model tier.
|
| 126 |
+
|
| 127 |
+
If the user has set ``user_tier_override`` in the state, that
|
| 128 |
+
takes precedence over the automatic classification.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
state: Current LangGraph state.
|
| 132 |
+
|
| 133 |
+
Returns:
|
| 134 |
+
State update with routing_decision, complexity_score, selected_tier.
|
| 135 |
+
"""
|
| 136 |
+
clinical_text: str = state.get("clinical_text", "")
|
| 137 |
+
entities: Dict[str, Any] = state.get("extracted_entities", {})
|
| 138 |
+
user_override: Optional[int] = state.get("user_tier_override")
|
| 139 |
+
|
| 140 |
+
# Run automatic classification
|
| 141 |
+
decision, score, auto_tier = _classify_complexity(clinical_text, entities)
|
| 142 |
+
|
| 143 |
+
# Apply manual override if present
|
| 144 |
+
if user_override in (1, 2):
|
| 145 |
+
selected_tier = user_override
|
| 146 |
+
logger.info(
|
| 147 |
+
"Manual tier override applied: Tier %d (auto would be Tier %d, score=%.2f)",
|
| 148 |
+
user_override, auto_tier, score,
|
| 149 |
+
)
|
| 150 |
+
else:
|
| 151 |
+
selected_tier = auto_tier
|
| 152 |
+
logger.info(
|
| 153 |
+
"Auto-routing: decision=%s, score=%.2f → Tier %d",
|
| 154 |
+
decision, score, selected_tier,
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
return {
|
| 158 |
+
"routing_decision": decision,
|
| 159 |
+
"complexity_score": round(score, 4),
|
| 160 |
+
"selected_tier": selected_tier,
|
| 161 |
+
}
|
agents/specialist.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Specialist Node — Tier-adaptive clinical reasoning with Chain-of-Thought.
|
| 3 |
+
|
| 4 |
+
Design patterns:
|
| 5 |
+
- Model Tiering: routes to Qwen3.5-9B (fast) or Qwen3.6-27B (deep)
|
| 6 |
+
- Reflexion: accepts critic feedback for iterative refinement
|
| 7 |
+
- Anti-Hallucination: system prompt strictly forbids inventing treatments
|
| 8 |
+
|
| 9 |
+
The specialist produces a structured recommendation with explicit
|
| 10 |
+
reasoning sections (Findings → Staging → Treatment → Recommendation).
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import logging
|
| 14 |
+
from typing import Dict, Any
|
| 15 |
+
|
| 16 |
+
from .state import AgentState
|
| 17 |
+
from .tools import call_tier_model, get_tier_spec
|
| 18 |
+
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# ---------------------------------------------------------------------------
|
| 23 |
+
# Prompt Engineering
|
| 24 |
+
# ---------------------------------------------------------------------------
|
| 25 |
+
|
| 26 |
+
_SYSTEM_PROMPT_TEMPLATE = """\
|
| 27 |
+
You are an expert clinical oncologist operating as part of the OncoAgent system.
|
| 28 |
+
Your task is to analyze the patient case and provide the most appropriate clinical
|
| 29 |
+
next steps based STRICTLY on the provided guidelines.
|
| 30 |
+
|
| 31 |
+
MODEL TIER: {tier_name} ({tier_description})
|
| 32 |
+
|
| 33 |
+
DIAGNOSTIC RIGOR POLICY:
|
| 34 |
+
1. You MUST verify if a definitive diagnosis (e.g., pathology report, biopsy) exists.
|
| 35 |
+
2. If diagnostic evidence is missing or inconclusive, your PRIMARY recommendation
|
| 36 |
+
MUST be the specific diagnostic procedure needed (e.g., "Esperar informe de biopsia",
|
| 37 |
+
"Realizar legrado diagnóstico").
|
| 38 |
+
3. You are STRICTLY FORBIDDEN from assuming cancer exists or jumping to treatment
|
| 39 |
+
protocols (surgery, chemo, radiation) if the pathology is not confirmed in the input.
|
| 40 |
+
|
| 41 |
+
ANTI-HALLUCINATION POLICY:
|
| 42 |
+
1. If the information is NOT explicitly in the guidelines, reply ONLY with:
|
| 43 |
+
"Información no concluyente en las guías provistas."
|
| 44 |
+
2. Do NOT invent dosages or protocols.
|
| 45 |
+
|
| 46 |
+
OUTPUT FORMAT (use this exact structure):
|
| 47 |
+
## Hallazgos Clínicos
|
| 48 |
+
[Summary of current patient presentation]
|
| 49 |
+
|
| 50 |
+
## Validación Diagnóstica
|
| 51 |
+
[State if pathology/biopsy is present and confirmed. If missing, specify what is needed.]
|
| 52 |
+
|
| 53 |
+
## Análisis de Estadificación
|
| 54 |
+
[Map findings to staging ONLY if diagnosis is confirmed. Otherwise, state why it's not possible.]
|
| 55 |
+
|
| 56 |
+
## Opciones de Manejo
|
| 57 |
+
[List clinical next steps or treatment options ONLY if appropriate for the diagnostic stage.]
|
| 58 |
+
|
| 59 |
+
## Recomendación Final
|
| 60 |
+
[The absolute next step for the clinician with confidence level]
|
| 61 |
+
|
| 62 |
+
Provide your recommendation in Spanish, clearly citing the guidelines.
|
| 63 |
+
IMPORTANT: Output your recommendation DIRECTLY. Do NOT wrap it in <think> tags."""
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
_USER_PROMPT_TEMPLATE = """\
|
| 67 |
+
Patient Information:
|
| 68 |
+
- Original Text: {clinical_text}
|
| 69 |
+
- Cancer Type: {cancer_type}
|
| 70 |
+
- Stage: {stage}
|
| 71 |
+
- Mutations: {mutations}
|
| 72 |
+
|
| 73 |
+
Clinical Guidelines Context:
|
| 74 |
+
{context}
|
| 75 |
+
|
| 76 |
+
{api_evidence}
|
| 77 |
+
|
| 78 |
+
{critic_feedback_section}
|
| 79 |
+
|
| 80 |
+
Based ONLY on the guidelines above, what are the recommended clinical next steps?"""
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _build_specialist_prompt(
|
| 84 |
+
state: AgentState,
|
| 85 |
+
) -> tuple[str, str]:
|
| 86 |
+
"""Build the system and user prompts for the specialist.
|
| 87 |
+
|
| 88 |
+
Incorporates critic feedback if this is a retry iteration.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
state: Current LangGraph state.
|
| 92 |
+
|
| 93 |
+
Returns:
|
| 94 |
+
Tuple of (system_prompt, user_prompt).
|
| 95 |
+
"""
|
| 96 |
+
tier = state.get("selected_tier", 1)
|
| 97 |
+
spec = get_tier_spec(tier)
|
| 98 |
+
|
| 99 |
+
system_prompt = _SYSTEM_PROMPT_TEMPLATE.format(
|
| 100 |
+
tier_name=spec.name,
|
| 101 |
+
tier_description=spec.description,
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
entities = state.get("extracted_entities", {})
|
| 105 |
+
context = "\n---\n".join(state.get("rag_context", []))
|
| 106 |
+
api_evidence = state.get("api_evidence_context", [])
|
| 107 |
+
|
| 108 |
+
# Format API evidence if available
|
| 109 |
+
api_section = ""
|
| 110 |
+
if api_evidence:
|
| 111 |
+
api_section = "Additional Evidence (Genomic/Trials):\n" + "\n".join(api_evidence)
|
| 112 |
+
|
| 113 |
+
# Inject critic feedback for retry iterations
|
| 114 |
+
critic_feedback = state.get("critic_feedback", "")
|
| 115 |
+
critic_attempts = state.get("critic_attempts", 0)
|
| 116 |
+
feedback_section = ""
|
| 117 |
+
if critic_attempts > 0 and critic_feedback:
|
| 118 |
+
feedback_section = (
|
| 119 |
+
f"\n⚠️ PREVIOUS ATTEMPT FEEDBACK (attempt {critic_attempts}):\n"
|
| 120 |
+
f"The following issues were identified in your previous recommendation. "
|
| 121 |
+
f"Please address them in this revision:\n{critic_feedback}\n"
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
user_prompt = _USER_PROMPT_TEMPLATE.format(
|
| 125 |
+
clinical_text=state.get("clinical_text", ""),
|
| 126 |
+
cancer_type=entities.get("cancer_type", "Unknown"),
|
| 127 |
+
stage=entities.get("stage", "Unknown"),
|
| 128 |
+
mutations=", ".join(entities.get("mutations", [])),
|
| 129 |
+
context=context,
|
| 130 |
+
api_evidence=api_section,
|
| 131 |
+
critic_feedback_section=feedback_section,
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
return system_prompt, user_prompt
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
# ---------------------------------------------------------------------------
|
| 138 |
+
# Specialist Node
|
| 139 |
+
# ---------------------------------------------------------------------------
|
| 140 |
+
|
| 141 |
+
def specialist_node(state: AgentState) -> Dict[str, Any]:
|
| 142 |
+
"""Generate a clinical recommendation using the tier-adaptive model.
|
| 143 |
+
|
| 144 |
+
If critic feedback exists in the state (retry iteration), the feedback
|
| 145 |
+
is injected into the prompt so the model can self-correct.
|
| 146 |
+
|
| 147 |
+
Args:
|
| 148 |
+
state: Current LangGraph state.
|
| 149 |
+
|
| 150 |
+
Returns:
|
| 151 |
+
State update with clinical_recommendation and reasoning_trace.
|
| 152 |
+
"""
|
| 153 |
+
context = state.get("rag_context", [])
|
| 154 |
+
tier = state.get("selected_tier", 1)
|
| 155 |
+
attempt = state.get("critic_attempts", 0)
|
| 156 |
+
|
| 157 |
+
# Guard: no context available
|
| 158 |
+
if not context:
|
| 159 |
+
return {
|
| 160 |
+
"clinical_recommendation": (
|
| 161 |
+
"Información no concluyente en las guías provistas. "
|
| 162 |
+
"No se encontró evidencia relevante en la base de datos clínica."
|
| 163 |
+
),
|
| 164 |
+
"reasoning_trace": "No RAG context available — safe fallback triggered.",
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
system_prompt, user_prompt = _build_specialist_prompt(state)
|
| 168 |
+
|
| 169 |
+
spec = get_tier_spec(tier)
|
| 170 |
+
logger.info(
|
| 171 |
+
"Specialist invoking %s (attempt %d, context chunks: %d)",
|
| 172 |
+
spec, attempt + 1, len(context),
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
try:
|
| 176 |
+
recommendation = call_tier_model(
|
| 177 |
+
tier=tier,
|
| 178 |
+
system_prompt=system_prompt,
|
| 179 |
+
user_prompt=user_prompt,
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
# Build reasoning trace for the critic
|
| 183 |
+
reasoning_trace = (
|
| 184 |
+
f"Tier: {spec.name} ({spec.model_id})\n"
|
| 185 |
+
f"Attempt: {attempt + 1}\n"
|
| 186 |
+
f"Context chunks: {len(context)}\n"
|
| 187 |
+
f"API evidence items: {len(state.get('api_evidence_context', []))}\n"
|
| 188 |
+
f"Recommendation length: {len(recommendation)} chars"
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
except RuntimeError as exc:
|
| 192 |
+
logger.error("Specialist inference failed: %s", exc)
|
| 193 |
+
recommendation = (
|
| 194 |
+
"Error en el sistema de inferencia. "
|
| 195 |
+
"No se pudo generar la recomendación clínica en este momento."
|
| 196 |
+
)
|
| 197 |
+
reasoning_trace = f"INFERENCE ERROR: {exc}"
|
| 198 |
+
|
| 199 |
+
# Detect if model returned the safe phrase
|
| 200 |
+
if "información no concluyente" in recommendation.lower():
|
| 201 |
+
recommendation = "Información no concluyente en las guías provistas."
|
| 202 |
+
|
| 203 |
+
return {
|
| 204 |
+
"clinical_recommendation": recommendation,
|
| 205 |
+
"reasoning_trace": reasoning_trace,
|
| 206 |
+
}
|
agents/state.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AgentState — Shared state schema for the OncoAgent LangGraph execution.
|
| 3 |
+
|
| 4 |
+
Design principles (inspired by Claude Code + Hermes Agent):
|
| 5 |
+
- Immutable input: ``clinical_text`` is never mutated.
|
| 6 |
+
- Additive outputs: each node writes to its own isolated keys.
|
| 7 |
+
- Deterministic routing: ``routing_decision`` and ``selected_tier``
|
| 8 |
+
are set by the Router node using structured logic, never free text.
|
| 9 |
+
- Per-patient memory: ``patient_id`` isolates session history.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from typing import TypedDict, Annotated, List, Dict, Any, Optional
|
| 13 |
+
import operator
|
| 14 |
+
from langchain_core.messages import BaseMessage
|
| 15 |
+
from langgraph.graph.message import add_messages
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class AgentState(TypedDict):
|
| 19 |
+
"""
|
| 20 |
+
Represents the state of the LangGraph execution for OncoAgent.
|
| 21 |
+
|
| 22 |
+
Sections are ordered by the pipeline stage that writes them.
|
| 23 |
+
Keys prefixed with ``#`` comments indicate which node owns each group.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
# ------------------------------------------------------------------ #
|
| 27 |
+
# 0. Session & Patient Context #
|
| 28 |
+
# ------------------------------------------------------------------ #
|
| 29 |
+
patient_id: str # Unique patient profile ID
|
| 30 |
+
session_id: str # Current session identifier
|
| 31 |
+
user_tier_override: Optional[int] # Manual tier override (1 or 2, None = auto)
|
| 32 |
+
messages: Annotated[List[BaseMessage], add_messages] # Chat history
|
| 33 |
+
|
| 34 |
+
# ------------------------------------------------------------------ #
|
| 35 |
+
# 1. Input (Immutable — set once at invocation) #
|
| 36 |
+
# ------------------------------------------------------------------ #
|
| 37 |
+
clinical_text: str
|
| 38 |
+
|
| 39 |
+
# ------------------------------------------------------------------ #
|
| 40 |
+
# 2. Router Node #
|
| 41 |
+
# ------------------------------------------------------------------ #
|
| 42 |
+
routing_decision: str # "simple" | "complex" | "insufficient"
|
| 43 |
+
selected_tier: int # 1 (Qwen 3.5 9B) or 2 (Qwen 3.6 27B)
|
| 44 |
+
complexity_score: float # 0.0–1.0 complexity estimate
|
| 45 |
+
|
| 46 |
+
# ------------------------------------------------------------------ #
|
| 47 |
+
# 3. Ingestion Node (PHI clean + entity extraction) #
|
| 48 |
+
# ------------------------------------------------------------------ #
|
| 49 |
+
extracted_entities: Dict[str, Any]
|
| 50 |
+
phi_detected: bool
|
| 51 |
+
|
| 52 |
+
# ------------------------------------------------------------------ #
|
| 53 |
+
# 4. Corrective RAG Node #
|
| 54 |
+
# ------------------------------------------------------------------ #
|
| 55 |
+
rag_context: List[str]
|
| 56 |
+
rag_sources: List[str]
|
| 57 |
+
graph_rag_context: List[str] # Clinical Knowledge Graph results
|
| 58 |
+
api_evidence_context: List[str] # CIViC / ClinicalTrials.gov results
|
| 59 |
+
rag_confidence: float # Mean cross-encoder score (0–1)
|
| 60 |
+
rag_retrieval_count: int # Results that passed the distance gate
|
| 61 |
+
rag_grading_pass_count: int # Documents graded RELEVANT by CRAG
|
| 62 |
+
rag_query_rewrites: int # Number of query rewrites performed
|
| 63 |
+
|
| 64 |
+
# ------------------------------------------------------------------ #
|
| 65 |
+
# 5. Specialist Node (Tier-adaptive reasoning) #
|
| 66 |
+
# ------------------------------------------------------------------ #
|
| 67 |
+
clinical_recommendation: str
|
| 68 |
+
reasoning_trace: str # Chain-of-thought breakdown
|
| 69 |
+
|
| 70 |
+
# ------------------------------------------------------------------ #
|
| 71 |
+
# 6. Critic Node (Reflexion loop) #
|
| 72 |
+
# ------------------------------------------------------------------ #
|
| 73 |
+
critic_verdict: str # "PASS" | "FAIL"
|
| 74 |
+
critic_feedback: str # Specific issues for specialist retry
|
| 75 |
+
critic_attempts: int # Current iteration count (max 2)
|
| 76 |
+
|
| 77 |
+
# ------------------------------------------------------------------ #
|
| 78 |
+
# 7. HITL Gate #
|
| 79 |
+
# ------------------------------------------------------------------ #
|
| 80 |
+
acuity_level: str # "low" | "medium" | "high"
|
| 81 |
+
hitl_required: bool # True if clinician approval needed
|
| 82 |
+
hitl_approved: bool # Set by clinician via UI interrupt
|
| 83 |
+
|
| 84 |
+
# ------------------------------------------------------------------ #
|
| 85 |
+
# 8. Formatter Node (final output) #
|
| 86 |
+
# ------------------------------------------------------------------ #
|
| 87 |
+
formatted_recommendation: str # Markdown-formatted for Gradio
|
| 88 |
+
confidence_report: Dict[str, Any] # Full metrics (tier, RAG, critic iters)
|
| 89 |
+
source_citations: List[str] # Formatted bibliography
|
| 90 |
+
|
| 91 |
+
# ------------------------------------------------------------------ #
|
| 92 |
+
# 9. Fallback Node #
|
| 93 |
+
# ------------------------------------------------------------------ #
|
| 94 |
+
fallback_reason: str # Why the system fell back to safe mode
|
| 95 |
+
|
| 96 |
+
# ------------------------------------------------------------------ #
|
| 97 |
+
# 10. Safety (legacy compat + validator output) #
|
| 98 |
+
# ------------------------------------------------------------------ #
|
| 99 |
+
safety_status: str
|
| 100 |
+
is_safe: bool
|
| 101 |
+
|
| 102 |
+
# ------------------------------------------------------------------ #
|
| 103 |
+
# 11. Error accumulator (append-only via operator.add) #
|
| 104 |
+
# ------------------------------------------------------------------ #
|
| 105 |
+
errors: Annotated[List[str], operator.add]
|
agents/tools.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Shared vLLM client and tier-aware model calling utilities.
|
| 3 |
+
|
| 4 |
+
All LLM inference across OncoAgent flows through this module,
|
| 5 |
+
ensuring consistent model selection, error handling, and
|
| 6 |
+
environment variable management.
|
| 7 |
+
|
| 8 |
+
Design inspired by:
|
| 9 |
+
- Hermes Agent: structured tool calling with JSON output
|
| 10 |
+
- Claude Code: deterministic harness separating LLM from execution
|
| 11 |
+
|
| 12 |
+
Production target: AMD Instinct MI300X via ROCm 7.2 + vLLM
|
| 13 |
+
Development fallback: Featherless.ai OpenAI-compatible API
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import re
|
| 18 |
+
import logging
|
| 19 |
+
from typing import Optional, Dict, Any, List
|
| 20 |
+
from dataclasses import dataclass
|
| 21 |
+
|
| 22 |
+
from openai import OpenAI
|
| 23 |
+
from dotenv import load_dotenv
|
| 24 |
+
|
| 25 |
+
load_dotenv()
|
| 26 |
+
|
| 27 |
+
logger = logging.getLogger(__name__)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# ---------------------------------------------------------------------------
|
| 31 |
+
# Tier Configuration
|
| 32 |
+
# ---------------------------------------------------------------------------
|
| 33 |
+
|
| 34 |
+
@dataclass(frozen=True)
|
| 35 |
+
class TierSpec:
|
| 36 |
+
"""Immutable specification for a model tier."""
|
| 37 |
+
|
| 38 |
+
tier_id: int
|
| 39 |
+
name: str
|
| 40 |
+
model_id: str
|
| 41 |
+
description: str
|
| 42 |
+
max_tokens: int
|
| 43 |
+
temperature: float
|
| 44 |
+
|
| 45 |
+
def __str__(self) -> str:
|
| 46 |
+
return f"Tier {self.tier_id}: {self.name} ({self.model_id})"
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# Production tier definitions — Qwen 3.5 / 3.6 as per project rules
|
| 50 |
+
TIER_SPECS: Dict[int, TierSpec] = {
|
| 51 |
+
1: TierSpec(
|
| 52 |
+
tier_id=1,
|
| 53 |
+
name="Speed Triage",
|
| 54 |
+
model_id=os.getenv("TIER1_MODEL_ID", "Qwen/Qwen3.5-9B"),
|
| 55 |
+
description="Fast model for initial triage and low-complexity cases.",
|
| 56 |
+
max_tokens=2048,
|
| 57 |
+
temperature=0.1,
|
| 58 |
+
),
|
| 59 |
+
2: TierSpec(
|
| 60 |
+
tier_id=2,
|
| 61 |
+
name="Deep Reasoning",
|
| 62 |
+
model_id=os.getenv("TIER2_MODEL_ID", "Qwen/Qwen3.6-27B"),
|
| 63 |
+
description="High-reasoning model for complex oncology cases and validation.",
|
| 64 |
+
max_tokens=4096,
|
| 65 |
+
temperature=0.0,
|
| 66 |
+
),
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# ---------------------------------------------------------------------------
|
| 71 |
+
# Qwen3 Thinking-Mode Handler
|
| 72 |
+
# ---------------------------------------------------------------------------
|
| 73 |
+
|
| 74 |
+
_THINK_PATTERN = re.compile(r"<think>.*?</think>", re.DOTALL)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _strip_thinking_tokens(text: str) -> str:
|
| 78 |
+
"""Remove Qwen3 <think>...</think> blocks from model output.
|
| 79 |
+
|
| 80 |
+
Qwen3 models use an internal reasoning mode that wraps chain-of-thought
|
| 81 |
+
in <think> tags. We preserve only the final answer for the pipeline.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
text: Raw model output potentially containing <think> blocks.
|
| 85 |
+
|
| 86 |
+
Returns:
|
| 87 |
+
Cleaned text with thinking blocks removed.
|
| 88 |
+
"""
|
| 89 |
+
cleaned = _THINK_PATTERN.sub("", text).strip()
|
| 90 |
+
# If everything was inside <think> tags, return the original
|
| 91 |
+
return cleaned if cleaned else text.strip()
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
# ---------------------------------------------------------------------------
|
| 95 |
+
# vLLM Client Singleton
|
| 96 |
+
# ---------------------------------------------------------------------------
|
| 97 |
+
|
| 98 |
+
_vllm_client: Optional[OpenAI] = None
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def get_vllm_client() -> OpenAI:
|
| 102 |
+
"""Return a cached OpenAI-compatible client pointing at vLLM.
|
| 103 |
+
|
| 104 |
+
Reads ``VLLM_API_BASE`` and ``VLLM_API_KEY`` from environment.
|
| 105 |
+
|
| 106 |
+
Returns:
|
| 107 |
+
OpenAI client configured for the local vLLM server.
|
| 108 |
+
"""
|
| 109 |
+
global _vllm_client
|
| 110 |
+
if _vllm_client is None:
|
| 111 |
+
api_base = os.getenv("VLLM_API_BASE", "http://localhost:8000/v1")
|
| 112 |
+
api_key = os.getenv("VLLM_API_KEY", "EMPTY")
|
| 113 |
+
_vllm_client = OpenAI(base_url=api_base, api_key=api_key)
|
| 114 |
+
logger.info("vLLM client initialised → %s", api_base)
|
| 115 |
+
return _vllm_client
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# ---------------------------------------------------------------------------
|
| 119 |
+
# Model ID Resolution (handles Featherless fallback for dev)
|
| 120 |
+
# ---------------------------------------------------------------------------
|
| 121 |
+
|
| 122 |
+
def _resolve_model_id(spec: TierSpec) -> str:
|
| 123 |
+
"""Resolve the actual model ID to use for API calls.
|
| 124 |
+
|
| 125 |
+
In production (local vLLM), we use the exact model ID.
|
| 126 |
+
In development (Featherless.ai), some models may not be available,
|
| 127 |
+
so we check for configured fallbacks.
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
spec: The TierSpec for the requested tier.
|
| 131 |
+
|
| 132 |
+
Returns:
|
| 133 |
+
The model ID string to pass to the API.
|
| 134 |
+
"""
|
| 135 |
+
api_base = os.getenv("VLLM_API_BASE", "http://localhost:8000/v1")
|
| 136 |
+
is_featherless = "featherless" in api_base.lower()
|
| 137 |
+
|
| 138 |
+
if is_featherless and spec.tier_id == 2:
|
| 139 |
+
# Qwen3.6-27B is not available on Featherless — use fallback
|
| 140 |
+
fallback = os.getenv("TIER2_FEATHERLESS_FALLBACK", "Qwen/Qwen3.5-27B")
|
| 141 |
+
logger.info(
|
| 142 |
+
"Featherless detected: Tier 2 fallback %s → %s",
|
| 143 |
+
spec.model_id, fallback,
|
| 144 |
+
)
|
| 145 |
+
return fallback
|
| 146 |
+
|
| 147 |
+
return spec.model_id
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
# ---------------------------------------------------------------------------
|
| 151 |
+
# Local Adapter Manager (PEFT — AMD MI300X only)
|
| 152 |
+
# ---------------------------------------------------------------------------
|
| 153 |
+
|
| 154 |
+
class LocalModelManager:
|
| 155 |
+
"""Singleton to manage local LoRA model loading and inference.
|
| 156 |
+
|
| 157 |
+
Only used on the AMD droplet with working ROCm/GPU drivers.
|
| 158 |
+
In development without GPU, this is skipped entirely.
|
| 159 |
+
"""
|
| 160 |
+
|
| 161 |
+
_instance = None
|
| 162 |
+
|
| 163 |
+
def __new__(cls):
|
| 164 |
+
if cls._instance is None:
|
| 165 |
+
cls._instance = super(LocalModelManager, cls).__new__(cls)
|
| 166 |
+
cls._instance.model = None
|
| 167 |
+
cls._instance.tokenizer = None
|
| 168 |
+
cls._instance.initialized = False
|
| 169 |
+
return cls._instance
|
| 170 |
+
|
| 171 |
+
def initialize(self) -> None:
|
| 172 |
+
"""Load the base model and LoRA adapters."""
|
| 173 |
+
if self.initialized:
|
| 174 |
+
return
|
| 175 |
+
|
| 176 |
+
try:
|
| 177 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 178 |
+
from peft import PeftModel
|
| 179 |
+
import torch
|
| 180 |
+
except ImportError:
|
| 181 |
+
logger.warning("Transformers/PEFT/Torch not installed. Local inference disabled.")
|
| 182 |
+
return
|
| 183 |
+
|
| 184 |
+
adapter_path = os.getenv("LOCAL_ADAPTER_PATH")
|
| 185 |
+
base_model_id = os.getenv("BASE_MODEL_ID", "Qwen/Qwen3.5-9B")
|
| 186 |
+
|
| 187 |
+
if not adapter_path or not os.path.exists(adapter_path):
|
| 188 |
+
logger.error("Local adapter path not found: %s", adapter_path)
|
| 189 |
+
return
|
| 190 |
+
|
| 191 |
+
logger.info("Loading base model %s + adapters %s...", base_model_id, adapter_path)
|
| 192 |
+
try:
|
| 193 |
+
self.tokenizer = AutoTokenizer.from_pretrained(
|
| 194 |
+
base_model_id, trust_remote_code=True,
|
| 195 |
+
)
|
| 196 |
+
base_model = AutoModelForCausalLM.from_pretrained(
|
| 197 |
+
base_model_id,
|
| 198 |
+
dtype=torch.bfloat16,
|
| 199 |
+
device_map="auto",
|
| 200 |
+
trust_remote_code=True,
|
| 201 |
+
)
|
| 202 |
+
self.model = PeftModel.from_pretrained(base_model, adapter_path)
|
| 203 |
+
self.model.eval()
|
| 204 |
+
self.initialized = True
|
| 205 |
+
logger.info("Local BF16 model ready on %s", os.getenv("DEVICE", "cuda"))
|
| 206 |
+
except Exception as exc:
|
| 207 |
+
logger.error("Failed to load local model: %s", exc)
|
| 208 |
+
|
| 209 |
+
def generate(
|
| 210 |
+
self,
|
| 211 |
+
system_prompt: str,
|
| 212 |
+
user_prompt: str,
|
| 213 |
+
max_tokens: int,
|
| 214 |
+
temperature: float,
|
| 215 |
+
) -> str:
|
| 216 |
+
"""Run inference using the loaded local model."""
|
| 217 |
+
if not self.initialized:
|
| 218 |
+
self.initialize()
|
| 219 |
+
if not self.initialized:
|
| 220 |
+
raise RuntimeError("Local model manager not initialized.")
|
| 221 |
+
|
| 222 |
+
import torch
|
| 223 |
+
|
| 224 |
+
messages = [
|
| 225 |
+
{"role": "system", "content": system_prompt},
|
| 226 |
+
{"role": "user", "content": user_prompt},
|
| 227 |
+
]
|
| 228 |
+
prompt_str = self.tokenizer.apply_chat_template(
|
| 229 |
+
messages, tokenize=False, add_generation_prompt=True,
|
| 230 |
+
)
|
| 231 |
+
inputs = self.tokenizer(text=prompt_str, return_tensors="pt").to("cuda")
|
| 232 |
+
|
| 233 |
+
with torch.no_grad():
|
| 234 |
+
outputs = self.model.generate(
|
| 235 |
+
**inputs,
|
| 236 |
+
max_new_tokens=max_tokens,
|
| 237 |
+
temperature=temperature,
|
| 238 |
+
do_sample=temperature > 0,
|
| 239 |
+
use_cache=True,
|
| 240 |
+
pad_token_id=self.tokenizer.pad_token_id,
|
| 241 |
+
)
|
| 242 |
+
generated_ids = outputs[:, inputs.input_ids.shape[1]:]
|
| 243 |
+
response = self.tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
|
| 244 |
+
return _strip_thinking_tokens(response)
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
_local_manager = LocalModelManager()
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
# ---------------------------------------------------------------------------
|
| 251 |
+
# Tier-Aware Model Calling
|
| 252 |
+
# ---------------------------------------------------------------------------
|
| 253 |
+
|
| 254 |
+
def call_tier_model(
|
| 255 |
+
tier: int,
|
| 256 |
+
system_prompt: str,
|
| 257 |
+
user_prompt: str,
|
| 258 |
+
max_tokens: Optional[int] = None,
|
| 259 |
+
temperature: Optional[float] = None,
|
| 260 |
+
json_mode: bool = False,
|
| 261 |
+
) -> str:
|
| 262 |
+
"""Call the appropriate model based on the selected tier.
|
| 263 |
+
|
| 264 |
+
This is the *single entry point* for all LLM inference in OncoAgent.
|
| 265 |
+
Every node must call this function instead of instantiating clients.
|
| 266 |
+
|
| 267 |
+
Flow:
|
| 268 |
+
1. If USE_LOCAL_ADAPTERS=true AND tier=1 → try local PEFT inference
|
| 269 |
+
2. If local fails or not enabled → route through vLLM/Featherless API
|
| 270 |
+
|
| 271 |
+
Args:
|
| 272 |
+
tier: Model tier (1 = fast 9B, 2 = deep 27B).
|
| 273 |
+
system_prompt: System-level instructions.
|
| 274 |
+
user_prompt: User-level content / query.
|
| 275 |
+
max_tokens: Override the tier's default max_tokens.
|
| 276 |
+
temperature: Override the tier's default temperature.
|
| 277 |
+
json_mode: If True, request JSON response format.
|
| 278 |
+
|
| 279 |
+
Returns:
|
| 280 |
+
The model's text response (stripped of thinking tokens).
|
| 281 |
+
|
| 282 |
+
Raises:
|
| 283 |
+
ValueError: If the tier is not 1 or 2.
|
| 284 |
+
RuntimeError: If the vLLM server is unreachable.
|
| 285 |
+
"""
|
| 286 |
+
spec = TIER_SPECS.get(tier)
|
| 287 |
+
if spec is None:
|
| 288 |
+
raise ValueError(f"Invalid tier {tier}. Must be 1 or 2.")
|
| 289 |
+
|
| 290 |
+
effective_max_tokens = max_tokens or spec.max_tokens
|
| 291 |
+
effective_temperature = temperature if temperature is not None else spec.temperature
|
| 292 |
+
|
| 293 |
+
logger.info(
|
| 294 |
+
"Calling %s (max_tokens=%d, temp=%.2f, json=%s)",
|
| 295 |
+
spec, effective_max_tokens, effective_temperature, json_mode,
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
# --- Path 1: Local LoRA adapters (MI300X only) ---
|
| 299 |
+
use_local = os.getenv("USE_LOCAL_ADAPTERS", "false").lower() == "true"
|
| 300 |
+
if tier == 1 and use_local:
|
| 301 |
+
try:
|
| 302 |
+
logger.info("Routing Tier 1 to local LoRA adapters...")
|
| 303 |
+
return _local_manager.generate(
|
| 304 |
+
system_prompt=system_prompt,
|
| 305 |
+
user_prompt=user_prompt,
|
| 306 |
+
max_tokens=effective_max_tokens,
|
| 307 |
+
temperature=effective_temperature,
|
| 308 |
+
)
|
| 309 |
+
except Exception as local_exc:
|
| 310 |
+
logger.warning("Local inference failed, falling back to API: %s", local_exc)
|
| 311 |
+
|
| 312 |
+
# --- Path 2: vLLM / Featherless API ---
|
| 313 |
+
model_id = _resolve_model_id(spec)
|
| 314 |
+
|
| 315 |
+
try:
|
| 316 |
+
client = get_vllm_client()
|
| 317 |
+
|
| 318 |
+
kwargs: Dict[str, Any] = {
|
| 319 |
+
"model": model_id,
|
| 320 |
+
"messages": [
|
| 321 |
+
{"role": "system", "content": system_prompt},
|
| 322 |
+
{"role": "user", "content": user_prompt},
|
| 323 |
+
],
|
| 324 |
+
"temperature": effective_temperature,
|
| 325 |
+
"max_tokens": effective_max_tokens,
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
if json_mode:
|
| 329 |
+
kwargs["response_format"] = {"type": "json_object"}
|
| 330 |
+
|
| 331 |
+
response = client.chat.completions.create(**kwargs)
|
| 332 |
+
raw_text = response.choices[0].message.content or ""
|
| 333 |
+
text = _strip_thinking_tokens(raw_text)
|
| 334 |
+
|
| 335 |
+
if not text:
|
| 336 |
+
logger.warning(
|
| 337 |
+
"Model returned empty response (raw_len=%d). "
|
| 338 |
+
"May be all <think> tokens. Returning raw.",
|
| 339 |
+
len(raw_text),
|
| 340 |
+
)
|
| 341 |
+
text = raw_text.strip() if raw_text else ""
|
| 342 |
+
|
| 343 |
+
logger.debug("Response length: %d chars", len(text))
|
| 344 |
+
return text
|
| 345 |
+
|
| 346 |
+
except Exception as exc:
|
| 347 |
+
logger.error("vLLM call failed for %s: %s", spec, exc)
|
| 348 |
+
raise RuntimeError(
|
| 349 |
+
f"Error connecting to vLLM ({model_id}): {exc}"
|
| 350 |
+
) from exc
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def get_tier_spec(tier: int) -> TierSpec:
|
| 354 |
+
"""Retrieve the TierSpec for the given tier number.
|
| 355 |
+
|
| 356 |
+
Args:
|
| 357 |
+
tier: 1 or 2.
|
| 358 |
+
|
| 359 |
+
Returns:
|
| 360 |
+
The corresponding TierSpec.
|
| 361 |
+
"""
|
| 362 |
+
spec = TIER_SPECS.get(tier)
|
| 363 |
+
if spec is None:
|
| 364 |
+
raise ValueError(f"Invalid tier {tier}. Available: {list(TIER_SPECS.keys())}")
|
| 365 |
+
return spec
|
app.py
CHANGED
|
@@ -235,6 +235,74 @@ label, .gr-input-label { color: #94a3b8 !important; }
|
|
| 235 |
padding: 12px; border-top: 1px solid #1e293b;
|
| 236 |
}
|
| 237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
/* Reduced motion */
|
| 239 |
@media (prefers-reduced-motion: reduce) {
|
| 240 |
*, *::before, *::after {
|
|
@@ -562,46 +630,87 @@ with gr.Blocks(
|
|
| 562 |
title="OncoAgent — Oncology Triage Demo",
|
| 563 |
theme=gr.themes.Base(),
|
| 564 |
) as demo:
|
| 565 |
-
#
|
| 566 |
-
gr.
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
|
| 603 |
# ── Event Handlers ────────────────────────────────────────────────
|
| 604 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 605 |
demo_btn.click(
|
| 606 |
fn=run_demo,
|
| 607 |
inputs=None,
|
|
|
|
| 235 |
padding: 12px; border-top: 1px solid #1e293b;
|
| 236 |
}
|
| 237 |
|
| 238 |
+
/* Landing Page */
|
| 239 |
+
.landing-page {
|
| 240 |
+
display: flex;
|
| 241 |
+
flex-direction: column;
|
| 242 |
+
align-items: center;
|
| 243 |
+
justify-content: center;
|
| 244 |
+
min-height: 80vh;
|
| 245 |
+
text-align: center;
|
| 246 |
+
background: radial-gradient(circle at center, rgba(14, 165, 233, 0.08) 0%, transparent 60%);
|
| 247 |
+
border-radius: 24px;
|
| 248 |
+
padding: 40px;
|
| 249 |
+
}
|
| 250 |
+
.hero-title {
|
| 251 |
+
font-family: 'Figtree', sans-serif;
|
| 252 |
+
font-size: 3.8rem;
|
| 253 |
+
font-weight: 800;
|
| 254 |
+
color: #f8fafc;
|
| 255 |
+
margin-bottom: 16px;
|
| 256 |
+
letter-spacing: -0.03em;
|
| 257 |
+
background: linear-gradient(135deg, #e0f2fe 0%, #38bdf8 100%);
|
| 258 |
+
-webkit-background-clip: text;
|
| 259 |
+
-webkit-text-fill-color: transparent;
|
| 260 |
+
}
|
| 261 |
+
.hero-subtitle {
|
| 262 |
+
font-size: 1.25rem;
|
| 263 |
+
color: #94a3b8;
|
| 264 |
+
max-width: 650px;
|
| 265 |
+
margin: 0 auto 32px auto;
|
| 266 |
+
line-height: 1.6;
|
| 267 |
+
}
|
| 268 |
+
.btn-launch {
|
| 269 |
+
background: linear-gradient(135deg, #0ea5e9, #0284c7) !important;
|
| 270 |
+
border: none !important; color: #fff !important;
|
| 271 |
+
font-size: 1.15rem !important; font-weight: 600 !important;
|
| 272 |
+
padding: 16px 42px !important; border-radius: 12px !important;
|
| 273 |
+
cursor: pointer !important;
|
| 274 |
+
transition: all 0.2s ease-out !important;
|
| 275 |
+
box-shadow: 0 8px 24px rgba(14, 165, 233, 0.3) !important;
|
| 276 |
+
}
|
| 277 |
+
.btn-launch:hover {
|
| 278 |
+
transform: translateY(-2px) !important;
|
| 279 |
+
box-shadow: 0 12px 32px rgba(14, 165, 233, 0.4) !important;
|
| 280 |
+
}
|
| 281 |
+
.features-grid {
|
| 282 |
+
display: flex;
|
| 283 |
+
gap: 24px;
|
| 284 |
+
margin-top: 56px;
|
| 285 |
+
justify-content: center;
|
| 286 |
+
flex-wrap: wrap;
|
| 287 |
+
}
|
| 288 |
+
.feature-card {
|
| 289 |
+
background: rgba(30, 41, 59, 0.6);
|
| 290 |
+
border: 1px solid rgba(51, 65, 85, 0.5);
|
| 291 |
+
border-radius: 16px;
|
| 292 |
+
padding: 24px;
|
| 293 |
+
width: 280px;
|
| 294 |
+
text-align: left;
|
| 295 |
+
backdrop-filter: blur(12px);
|
| 296 |
+
transition: transform 0.2s ease, border-color 0.2s ease;
|
| 297 |
+
}
|
| 298 |
+
.feature-card:hover {
|
| 299 |
+
transform: translateY(-4px);
|
| 300 |
+
border-color: rgba(14, 165, 233, 0.4);
|
| 301 |
+
}
|
| 302 |
+
.feature-icon { font-size: 2rem; margin-bottom: 14px; }
|
| 303 |
+
.feature-title { color: #f1f5f9; font-weight: 600; margin-bottom: 8px; font-size: 1.1rem; }
|
| 304 |
+
.feature-desc { color: #64748b; font-size: 0.88rem; line-height: 1.5; }
|
| 305 |
+
|
| 306 |
/* Reduced motion */
|
| 307 |
@media (prefers-reduced-motion: reduce) {
|
| 308 |
*, *::before, *::after {
|
|
|
|
| 630 |
title="OncoAgent — Oncology Triage Demo",
|
| 631 |
theme=gr.themes.Base(),
|
| 632 |
) as demo:
|
| 633 |
+
# ── Landing Page ──────────────────────────────────────────────────
|
| 634 |
+
with gr.Column(elem_classes=["landing-page"], visible=True) as landing_page:
|
| 635 |
+
gr.HTML("""
|
| 636 |
+
<div class="hero-title">🧬 OncoAgent</div>
|
| 637 |
+
<div class="hero-subtitle">
|
| 638 |
+
An open-source, multi-agent AI system designed for clinical oncology triage.
|
| 639 |
+
Powered by AMD Instinct™ MI300X, LangGraph, and specialized Qwen models.
|
| 640 |
+
</div>
|
| 641 |
+
""")
|
| 642 |
+
|
| 643 |
+
launch_btn = gr.Button("🚀 Launch Demo", elem_classes=["btn-launch"], size="lg")
|
| 644 |
+
|
| 645 |
+
gr.HTML("""
|
| 646 |
+
<div class="features-grid">
|
| 647 |
+
<div class="feature-card">
|
| 648 |
+
<div class="feature-icon">📚</div>
|
| 649 |
+
<div class="feature-title">Corrective RAG</div>
|
| 650 |
+
<div class="feature-desc">Grounded in 170+ NCCN & ESMO guidelines with distance-gating to prevent hallucinations.</div>
|
| 651 |
+
</div>
|
| 652 |
+
<div class="feature-card">
|
| 653 |
+
<div class="feature-icon">🧠</div>
|
| 654 |
+
<div class="feature-title">Multi-Agent Reasoning</div>
|
| 655 |
+
<div class="feature-desc">Tiered architecture (Qwen3.5-9B Router + Qwen3.6-27B Specialist) for complex clinical analysis.</div>
|
| 656 |
+
</div>
|
| 657 |
+
<div class="feature-card">
|
| 658 |
+
<div class="feature-icon">🛡️</div>
|
| 659 |
+
<div class="feature-title">Clinical Safety</div>
|
| 660 |
+
<div class="feature-desc">Reflexion loops validate outputs against strict medical criteria before clinician review.</div>
|
| 661 |
+
</div>
|
| 662 |
+
</div>
|
| 663 |
+
""")
|
| 664 |
+
|
| 665 |
+
# ── Main App ──────────────────────────────────────────────────────
|
| 666 |
+
with gr.Column(visible=False) as app_page:
|
| 667 |
+
# Header
|
| 668 |
+
gr.HTML(HEADER_HTML)
|
| 669 |
+
gr.HTML(INFO_HTML)
|
| 670 |
+
|
| 671 |
+
# Chat
|
| 672 |
+
chatbot = gr.Chatbot(
|
| 673 |
+
type="messages",
|
| 674 |
+
label="Clinical Triage Chat",
|
| 675 |
+
height=520,
|
| 676 |
+
show_label=False,
|
| 677 |
+
show_copy_button=True,
|
| 678 |
+
render_markdown=True,
|
| 679 |
+
elem_classes=["card"],
|
| 680 |
+
)
|
| 681 |
+
|
| 682 |
+
# Controls
|
| 683 |
+
with gr.Row():
|
| 684 |
+
with gr.Column(scale=3):
|
| 685 |
+
txt = gr.Textbox(
|
| 686 |
+
placeholder="Enter a clinical case or click '▶ View Demo'...",
|
| 687 |
+
show_label=False,
|
| 688 |
+
lines=2,
|
| 689 |
+
max_lines=5,
|
| 690 |
+
)
|
| 691 |
+
with gr.Column(scale=1, min_width=180):
|
| 692 |
+
demo_btn = gr.Button(
|
| 693 |
+
"▶ View Demo",
|
| 694 |
+
elem_classes=["btn-demo"],
|
| 695 |
+
size="lg",
|
| 696 |
+
)
|
| 697 |
+
|
| 698 |
+
with gr.Row():
|
| 699 |
+
send_btn = gr.Button("Send", elem_classes=["btn-primary"], size="sm")
|
| 700 |
+
clear_btn = gr.Button("🗑 Clear", variant="secondary", size="sm")
|
| 701 |
+
|
| 702 |
+
# Footer
|
| 703 |
+
gr.HTML(FOOTER_HTML)
|
| 704 |
|
| 705 |
# ── Event Handlers ────────────────────────────────────────────────
|
| 706 |
|
| 707 |
+
# Landing page navigation
|
| 708 |
+
launch_btn.click(
|
| 709 |
+
fn=lambda: [gr.update(visible=False), gr.update(visible=True)],
|
| 710 |
+
inputs=None,
|
| 711 |
+
outputs=[landing_page, app_page],
|
| 712 |
+
)
|
| 713 |
+
|
| 714 |
demo_btn.click(
|
| 715 |
fn=run_demo,
|
| 716 |
inputs=None,
|
config.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{'version': '6.14.0', 'api_prefix': '/gradio_api', 'mode': 'blocks', 'app_id': 4110450775379994823, 'dev_mode': False, 'vibe_mode': False, 'analytics_enabled': True, 'components': [{'id': 1, 'type': 'html', 'props': {'_retryable': False, '_undoable': False, 'likeable': False, 'streamable': False, 'value': '<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700&family=Inter:wght@300;400;500;600&display=swap">', 'html_template': '${value}', 'css_template': '', 'js_on_load': "element.addEventListener('click', function() { trigger('click') });", 'apply_default_css': True, 'show_label': False, 'visible': True, 'elem_classes': [], 'preserved_by_key': ['value'], 'container': False, 'padding': False, 'autoscroll': False, 'buttons': [], 'props': {}, 'name': 'html', '_selectable': False, 'component_class_name': 'HTML'}, 'skip_api': False, 'component_class_id': 'af5f63fae9620e1007439226451e1707e9a6016bcb073dc5a0b6433126cda1fc', 'key': None, 'api_info': {'type': 'string'}, 'api_info_as_input': {'type': 'string'}, 'api_info_as_output': {'type': 'string'}, 'example_inputs': '<p>Hello</p>'}, {'id': 2, 'type': 'html', 'props': {'_retryable': False, '_undoable': False, 'likeable': False, 'streamable': False, 'value': "<div class='header-bar'><span class='brand-name'>OncoAgent</span><span class='hw-badge'>AMD Instinct MI300X</span></div>", 'html_template': '${value}', 'css_template': '', 'js_on_load': "element.addEventListener('click', function() { trigger('click') });", 'apply_default_css': True, 'show_label': False, 'visible': True, 'elem_classes': [], 'preserved_by_key': ['value'], 'container': False, 'padding': False, 'autoscroll': False, 'buttons': [], 'props': {}, 'name': 'html', '_selectable': False, 'component_class_name': 'HTML'}, 'skip_api': False, 'component_class_id': 'af5f63fae9620e1007439226451e1707e9a6016bcb073dc5a0b6433126cda1fc', 'key': None, 'api_info': {'type': 'string'}, 'api_info_as_input': {'type': 'string'}, 'api_info_as_output': {'type': 'string'}, 'example_inputs': '<p>Hello</p>'}, {'id': 3, 'type': 'row', 'props': {'variant': 'default', 'visible': True, 'elem_classes': [], 'equal_height': False, 'show_progress': False, 'preserved_by_key': [], 'name': 'row'}, 'skip_api': True, 'component_class_id': '6f8a6130c432a547a95664f7f2f2a01fd8019e8e9b05282a61688092ea105a01', 'key': None}, {'id': 4, 'type': 'column', 'props': {'scale': 1, 'min_width': 280, 'variant': 'default', 'visible': True, 'elem_classes': [], 'show_progress': False, 'preserved_by_key': [], 'name': 'column'}, 'skip_api': True, 'component_class_id': '0fb38a7a679d0444705b8d6520b557d7fcfac8cb0bd868952cdb1503899457a3', 'key': None}, {'id': 5, 'type': 'column', 'props': {'scale': 1, 'min_width': 320, 'variant': 'default', 'visible': True, 'elem_classes': ['card'], 'show_progress': False, 'preserved_by_key': [], 'name': 'column'}, 'skip_api': True, 'component_class_id': '0fb38a7a679d0444705b8d6520b557d7fcfac8cb0bd868952cdb1503899457a3', 'key': None}, {'id': 6, 'type': 'html', 'props': {'_retryable': False, '_undoable': False, 'likeable': False, 'streamable': False, 'value': "<div class='section-title'>Session</div>", 'html_template': '${value}', 'css_template': '', 'js_on_load': "element.addEventListener('click', function() { trigger('click') });", 'apply_default_css': True, 'show_label': False, 'visible': True, 'elem_classes': [], 'preserved_by_key': ['value'], 'container': False, 'padding': False, 'autoscroll': False, 'buttons': [], 'props': {}, 'name': 'html', '_selectable': False, 'component_class_name': 'HTML'}, 'skip_api': False, 'component_class_id': 'af5f63fae9620e1007439226451e1707e9a6016bcb073dc5a0b6433126cda1fc', 'key': None, 'api_info': {'type': 'string'}, 'api_info_as_input': {'type': 'string'}, 'api_info_as_output': {'type': 'string'}, 'example_inputs': '<p>Hello</p>'}, {'id': 7, 'type': 'textbox', 'props': {'value': 'PT-5659', 'type': 'text', 'lines': 1, 'label': 'Patient ID', 'info': 'Unique session for memory isolation', 'show_label': True, 'container': True, 'min_width': 160, 'interactive': True, 'visible': True, 'autofocus': False, 'autoscroll': True, 'elem_classes': [], 'preserved_by_key': ['value'], 'rtl': False, 'buttons': [], 'submit_btn': False, 'stop_btn': False, 'name': 'textbox', '_selectable': False}, 'skip_api': False, 'component_class_id': 'de54b6fd6ce8622ae36d11c1cbd43b965ca46f0c1fe283a7cec562dcb91a3208', 'key': None, 'api_info': {'type': 'string'}, 'api_info_as_input': {'type': 'string'}, 'api_info_as_output': {'type': 'string'}, 'example_inputs': 'Hello!!'}, {'id': 8, 'type': 'dropdown', 'props': {'choices': [('auto', 'auto'), ('9b', '9b'), ('27b', '27b')], 'value': 'auto', 'type': 'value', 'allow_custom_value': False, 'filterable': True, 'label': 'Model Tier', 'info': 'Auto-routes based on case complexity', 'show_label': True, 'container': True, 'min_width': 160, 'visible': True, 'elem_classes': [], 'preserved_by_key': ['value'], 'buttons': [], 'name': 'dropdown', '_selectable': False}, 'skip_api': False, 'component_class_id': '2e97f31499001547c4b27c45798eb5990573be2b0b8f11854c47c3b044b07aab', 'key': None, 'api_info': {'type': 'string', 'enum': ['auto', '9b', '27b']}, 'api_info_as_input': {'type': 'string', 'enum': ['auto', '9b', '27b']}, 'api_info_as_output': {'type': 'string', 'enum': ['auto', '9b', '27b']}, 'example_inputs': 'auto'}, {'id': 9, 'type': 'form', 'props': {'scale': 0, 'min_width': 0, 'preserved_by_key': [], 'name': 'form'}, 'skip_api': True, 'component_class_id': 'aa68c082b9d7ec5a5fe2959e3c704e535742029261408360fcdbad9c6db4eb31', 'key': None}, {'id': 10, 'type': 'row', 'props': {'variant': 'default', 'visible': True, 'elem_classes': [], 'equal_height': False, 'show_progress': False, 'preserved_by_key': [], 'name': 'row'}, 'skip_api': True, 'component_class_id': '6f8a6130c432a547a95664f7f2f2a01fd8019e8e9b05282a61688092ea105a01', 'key': None}, {'id': 11, 'type': 'column', 'props': {'scale': 1, 'min_width': 100, 'variant': 'default', 'visible': True, 'elem_classes': ['kpi-tile'], 'show_progress': False, 'preserved_by_key': [], 'name': 'column'}, 'skip_api': True, 'component_class_id': '0fb38a7a679d0444705b8d6520b557d7fcfac8cb0bd868952cdb1503899457a3', 'key': None}, {'id': 12, 'type': 'html', 'props': {'_retryable': False, '_undoable': False, 'likeable': False, 'streamable': False, 'value': "<div class='kpi-label'>Confidence</div><div class='kpi-value' id='kpi-confidence'>—</div>", 'html_template': '${value}', 'css_template': '', 'js_on_load': "element.addEventListener('click', function() { trigger('click') });", 'apply_default_css': True, 'show_label': False, 'visible': True, 'elem_classes': [], 'preserved_by_key': ['value'], 'container': False, 'padding': False, 'autoscroll': False, 'buttons': [], 'props': {}, 'name': 'html', '_selectable': False, 'component_class_name': 'HTML'}, 'skip_api': False, 'component_class_id': 'af5f63fae9620e1007439226451e1707e9a6016bcb073dc5a0b6433126cda1fc', 'key': None, 'api_info': {'type': 'string'}, 'api_info_as_input': {'type': 'string'}, 'api_info_as_output': {'type': 'string'}, 'example_inputs': '<p>Hello</p>'}, {'id': 13, 'type': 'label', 'props': {'value': {}, 'label': 'Confidence', 'show_label': True, 'container': True, 'min_width': 160, 'visible': False, 'elem_classes': [], 'preserved_by_key': ['value'], 'show_heading': True, 'buttons': [], 'name': 'label', '_selectable': False}, 'skip_api': False, 'component_class_id': 'e06edc5731c4fc699cba51f556272631f237cf9443d7c1bb4d06c4b7556aa63a', 'key': None, 'api_info': {'$defs': {'LabelConfidence': {'properties': {'label': {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Label'}, 'confidence': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Confidence'}}, 'title': 'LabelConfidence', 'type': 'object'}}, 'properties': {'label': {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Label'}, 'confidences': {'anyOf': [{'items': {'$ref': '#/$defs/LabelConfidence'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Confidences'}}, 'title': 'LabelData', 'type': 'object', 'additional_description': None}, 'api_info_as_input': {'$defs': {'LabelConfidence': {'properties': {'label': {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Label'}, 'confidence': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Confidence'}}, 'title': 'LabelConfidence', 'type': 'object'}}, 'properties': {'label': {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Label'}, 'confidences': {'anyOf': [{'items': {'$ref': '#/$defs/LabelConfidence'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Confidences'}}, 'title': 'LabelData', 'type': 'object', 'additional_description': None}, 'api_info_as_output': {'$defs': {'LabelConfidence': {'properties': {'label': {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Label'}, 'confidence': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Confidence'}}, 'title': 'LabelConfidence', 'type': 'object'}}, 'properties': {'label': {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Label'}, 'confidences': {'anyOf': [{'items': {'$ref': '#/$defs/LabelConfidence'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Confidences'}}, 'title': 'LabelData', 'type': 'object', 'additional_description': None}, 'example_inputs': {'label': 'Cat', 'confidences': [{'label': 'cat', 'confidence': 0.9}, {'label': 'dog', 'confidence': 0.1}]}}, {'id': 14, 'type': 'column', 'props': {'scale': 1, 'min_width': 100, 'variant': 'default', 'visible': True, 'elem_classes': ['kpi-tile'], 'show_progress': False, 'preserved_by_key': [], 'name': 'column'}, 'skip_api': True, 'component_class_id': '0fb38a7a679d0444705b8d6520b557d7fcfac8cb0bd868952cdb1503899457a3', 'key': None}, {'id': 15, 'type': 'html', 'props': {'_retryable': False, '_undoable': False, 'likeable': False, 'streamable': False, 'value': "<div class='kpi-label'>Sources</div><div class='kpi-value' id='kpi-sources'>—</div>", 'html_template': '${value}', 'css_template': '', 'js_on_load': "element.addEventListener('click', function() { trigger('click') });", 'apply_default_css': True, 'show_label': False, 'visible': True, 'elem_classes': [], 'preserved_by_key': ['value'], 'container': False, 'padding': False, 'autoscroll': False, 'buttons': [], 'props': {}, 'name': 'html', '_selectable': False, 'component_class_name': 'HTML'}, 'skip_api': False, 'component_class_id': 'af5f63fae9620e1007439226451e1707e9a6016bcb073dc5a0b6433126cda1fc', 'key': None, 'api_info': {'type': 'string'}, 'api_info_as_input': {'type': 'string'}, 'api_info_as_output': {'type': 'string'}, 'example_inputs': '<p>Hello</p>'}, {'id': 16, 'type': 'label', 'props': {'value': {}, 'label': 'Sources', 'show_label': True, 'container': True, 'min_width': 160, 'visible': False, 'elem_classes': [], 'preserved_by_key': ['value'], 'show_heading': True, 'buttons': [], 'name': 'label', '_selectable': False}, 'skip_api': False, 'component_class_id': 'e06edc5731c4fc699cba51f556272631f237cf9443d7c1bb4d06c4b7556aa63a', 'key': None, 'api_info': {'$defs': {'LabelConfidence': {'properties': {'label': {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Label'}, 'confidence': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Confidence'}}, 'title': 'LabelConfidence', 'type': 'object'}}, 'properties': {'label': {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Label'}, 'confidences': {'anyOf': [{'items': {'$ref': '#/$defs/LabelConfidence'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Confidences'}}, 'title': 'LabelData', 'type': 'object', 'additional_description': None}, 'api_info_as_input': {'$defs': {'LabelConfidence': {'properties': {'label': {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Label'}, 'confidence': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Confidence'}}, 'title': 'LabelConfidence', 'type': 'object'}}, 'properties': {'label': {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Label'}, 'confidences': {'anyOf': [{'items': {'$ref': '#/$defs/LabelConfidence'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Confidences'}}, 'title': 'LabelData', 'type': 'object', 'additional_description': None}, 'api_info_as_output': {'$defs': {'LabelConfidence': {'properties': {'label': {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Label'}, 'confidence': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Confidence'}}, 'title': 'LabelConfidence', 'type': 'object'}}, 'properties': {'label': {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}, {'type': 'null'}], 'default': None, 'title': 'Label'}, 'confidences': {'anyOf': [{'items': {'$ref': '#/$defs/LabelConfidence'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Confidences'}}, 'title': 'LabelData', 'type': 'object', 'additional_description': None}, 'example_inputs': {'label': 'Cat', 'confidences': [{'label': 'cat', 'confidence': 0.9}, {'label': 'dog', 'confidence': 0.1}]}}, {'id': 17, 'type': 'tabs', 'props': {'visible': True, 'elem_classes': ['card'], 'preserved_by_key': [], 'name': 'tabs'}, 'skip_api': True, 'component_class_id': 'e3f75337ef6662053675c6300cd49c9ac252225778c27231c96a030da41e3a85', 'key': None}, {'id': 18, 'type': 'tabitem', 'props': {'label': 'Guidelines', 'visible': True, 'interactive': True, 'elem_classes': [], 'preserved_by_key': [], 'render_children': False, 'name': 'tab'}, 'skip_api': True, 'component_class_id': '0989c1da084cf64f1899851572faada48fa5efed022fd0d58d24f61eb6d9142d', 'key': None}, {'id': 19, 'type': 'markdown', 'props': {'value': 'NCCN and ESMO guideline evidence will appear here.', 'show_label': False, 'rtl': False, 'latex_delimiters': [{'left': '$$', 'right': '$$', 'display': True}], 'visible': True, 'elem_classes': [], 'preserved_by_key': ['value'], 'sanitize_html': True, 'line_breaks': False, 'header_links': False, 'container': False, 'padding': False, 'name': 'markdown', '_selectable': False}, 'skip_api': False, 'component_class_id': 'fd30cc896cca4ca584ffd40aa2efc60d9820bc905902b7c1d2b0c228f8aa7654', 'key': None, 'api_info': {'type': 'string'}, 'api_info_as_input': {'type': 'string'}, 'api_info_as_output': {'type': 'string'}, 'example_inputs': '# Hello!'}, {'id': 20, 'type': 'tabitem', 'props': {'label': 'Knowledge Graph', 'visible': True, 'interactive': True, 'elem_classes': [], 'preserved_by_key': [], 'render_children': False, 'name': 'tab'}, 'skip_api': True, 'component_class_id': '0989c1da084cf64f1899851572faada48fa5efed022fd0d58d24f61eb6d9142d', 'key': None}, {'id': 21, 'type': 'markdown', 'props': {'value': 'Knowledge graph connections will appear here.', 'show_label': False, 'rtl': False, 'latex_delimiters': [{'left': '$$', 'right': '$$', 'display': True}], 'visible': True, 'elem_classes': [], 'preserved_by_key': ['value'], 'sanitize_html': True, 'line_breaks': False, 'header_links': False, 'container': False, 'padding': False, 'name': 'markdown', '_selectable': False}, 'skip_api': False, 'component_class_id': 'fd30cc896cca4ca584ffd40aa2efc60d9820bc905902b7c1d2b0c228f8aa7654', 'key': None, 'api_info': {'type': 'string'}, 'api_info_as_input': {'type': 'string'}, 'api_info_as_output': {'type': 'string'}, 'example_inputs': '# Hello!'}, {'id': 22, 'type': 'tabitem', 'props': {'label': 'API Evidence', 'visible': True, 'interactive': True, 'elem_classes': [], 'preserved_by_key': [], 'render_children': False, 'name': 'tab'}, 'skip_api': True, 'component_class_id': '0989c1da084cf64f1899851572faada48fa5efed022fd0d58d24f61eb6d9142d', 'key': None}, {'id': 23, 'type': 'markdown', 'props': {'value': 'Real-time data from CIViC and ClinicalTrials.gov.', 'show_label': False, 'rtl': False, 'latex_delimiters': [{'left': '$$', 'right': '$$', 'display': True}], 'visible': True, 'elem_classes': [], 'preserved_by_key': ['value'], 'sanitize_html': True, 'line_breaks': False, 'header_links': False, 'container': False, 'padding': False, 'name': 'markdown', '_selectable': False}, 'skip_api': False, 'component_class_id': 'fd30cc896cca4ca584ffd40aa2efc60d9820bc905902b7c1d2b0c228f8aa7654', 'key': None, 'api_info': {'type': 'string'}, 'api_info_as_input': {'type': 'string'}, 'api_info_as_output': {'type': 'string'}, 'example_inputs': '# Hello!'}, {'id': 24, 'type': 'column', 'props': {'scale': 1, 'min_width': 320, 'variant': 'default', 'visible': True, 'elem_classes': ['card'], 'show_progress': False, 'preserved_by_key': [], 'name': 'column'}, 'skip_api': True, 'component_class_id': '0fb38a7a679d0444705b8d6520b557d7fcfac8cb0bd868952cdb1503899457a3', 'key': None}, {'id': 25, 'type': 'html', 'props': {'_retryable': False, '_undoable': False, 'likeable': False, 'streamable': False, 'value': "<div class='section-title'>System Status</div>", 'html_template': '${value}', 'css_template': '', 'js_on_load': "element.addEventListener('click', function() { trigger('click') });", 'apply_default_css': True, 'show_label': False, 'visible': True, 'elem_classes': [], 'preserved_by_key': ['value'], 'container': False, 'padding': False, 'autoscroll': False, 'buttons': [], 'props': {}, 'name': 'html', '_selectable': False, 'component_class_name': 'HTML'}, 'skip_api': False, 'component_class_id': 'af5f63fae9620e1007439226451e1707e9a6016bcb073dc5a0b6433126cda1fc', 'key': None, 'api_info': {'type': 'string'}, 'api_info_as_input': {'type': 'string'}, 'api_info_as_output': {'type': 'string'}, 'example_inputs': '<p>Hello</p>'}, {'id': 26, 'type': 'markdown', 'props': {'value': "<div class='status-bar'>System ready.</div>", 'show_label': False, 'rtl': False, 'latex_delimiters': [{'left': '$$', 'right': '$$', 'display': True}], 'visible': True, 'elem_id': 'status-box', 'elem_classes': [], 'preserved_by_key': ['value'], 'sanitize_html': True, 'line_breaks': False, 'header_links': False, 'container': False, 'padding': False, 'name': 'markdown', '_selectable': False}, 'skip_api': False, 'component_class_id': 'fd30cc896cca4ca584ffd40aa2efc60d9820bc905902b7c1d2b0c228f8aa7654', 'key': None, 'api_info': {'type': 'string'}, 'api_info_as_input': {'type': 'string'}, 'api_info_as_output': {'type': 'string'}, 'example_inputs': '# Hello!'}, {'id': 27, 'type': 'column', 'props': {'scale': 3, 'min_width': 320, 'variant': 'default', 'visible': True, 'elem_classes': [], 'show_progress': False, 'preserved_by_key': [], 'name': 'column'}, 'skip_api': True, 'component_class_id': '0fb38a7a679d0444705b8d6520b557d7fcfac8cb0bd868952cdb1503899457a3', 'key': None}, {'id': 28, 'type': 'column', 'props': {'scale': 1, 'min_width': 600, 'variant': 'default', 'visible': True, 'elem_classes': ['card'], 'show_progress': False, 'preserved_by_key': [], 'name': 'column'}, 'skip_api': True, 'component_class_id': '0fb38a7a679d0444705b8d6520b557d7fcfac8cb0bd868952cdb1503899457a3', 'key': None}, {'id': 29, 'type': 'chatbot', 'props': {'_undoable': False, '_retryable': False, 'likeable': False, 'value': [], 'label': 'OncoAgent', 'show_label': False, 'container': True, 'min_width': 160, 'visible': True, 'elem_classes': ['gr-chatbot'], 'autoscroll': True, 'preserved_by_key': ['value'], 'height': 620, 'resizable': False, 'latex_delimiters': [{'left': '$$', 'right': '$$', 'display': True}], 'rtl': False, 'buttons': ['share', 'copy', 'copy_all'], 'avatar_images': [None, None], 'sanitize_html': True, 'render_markdown': True, 'feedback_options': ['Like', 'Dislike'], 'line_breaks': True, 'allow_file_downloads': True, 'group_consecutive_messages': True, 'allow_tags': True, 'like_user_message': False, 'name': 'chatbot', '_selectable': False}, 'skip_api': False, 'component_class_id': '6f1aa2d606e01e101fc71adbda61e826828d4b1a485f1b6c746327585193dfb9', 'key': None, 'api_info': {'$defs': {'ComponentMessage': {'properties': {'component': {'title': 'Component', 'type': 'string'}, 'value': {'title': 'Value'}, 'constructor_args': {'additionalProperties': True, 'title': 'Constructor Args', 'type': 'object'}, 'props': {'additionalProperties': True, 'title': 'Props', 'type': 'object'}, 'type': {'const': 'component', 'default': 'component', 'title': 'Type', 'type': 'string'}}, 'required': ['component', 'value', 'constructor_args', 'props'], 'title': 'ComponentMessage', 'type': 'object'}, 'FileData': {'description': 'The FileData class is a subclass of the GradioModel class that represents a file object within a Gradio interface. It is used to store file data and metadata when a file is uploaded.\n\nAttributes:\n path: The server file path where the file is stored.\n url: The normalized server URL pointing to the file.\n size: The size of the file in bytes.\n orig_name: The original filename before upload.\n mime_type: The MIME type of the file.\n is_stream: Indicates whether the file is a stream.\n meta: Additional metadata used internally (should not be changed).', 'properties': {'path': {'title': 'Path', 'type': 'string'}, 'url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Url'}, 'size': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'title': 'Size'}, 'orig_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Orig Name'}, 'mime_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Mime Type'}, 'is_stream': {'default': False, 'title': 'Is Stream', 'type': 'boolean'}, 'meta': {'$ref': '#/$defs/FileDataMeta'}}, 'required': ['path'], 'title': 'FileData', 'type': 'object'}, 'FileDataMeta': {'properties': {'_type': {'const': 'gradio.FileData', 'title': 'Type', 'type': 'string'}}, 'required': ['_type'], 'title': 'FileDataMeta', 'type': 'object'}, 'FileMessage': {'properties': {'file': {'$ref': '#/$defs/FileData'}, 'alt_text': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Alt Text'}, 'type': {'const': 'file', 'default': 'file', 'title': 'Type', 'type': 'string'}}, 'required': ['file'], 'title': 'FileMessage', 'type': 'object'}, 'Message': {'properties': {'role': {'title': 'Role', 'type': 'string'}, 'metadata': {'anyOf': [{'$ref': '#/$defs/MetadataDict'}, {'type': 'null'}], 'default': None}, 'content': {'items': {'anyOf': [{'$ref': '#/$defs/TextMessage'}, {'$ref': '#/$defs/FileMessage'}, {'$ref': '#/$defs/ComponentMessage'}]}, 'title': 'Content', 'type': 'array'}, 'options': {'anyOf': [{'items': {'$ref': '#/$defs/OptionDict'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Options'}}, 'required': ['role', 'content'], 'title': 'Message', 'type': 'object'}, 'MetadataDict': {'description': 'A typed dictionary to represent metadata for a message in the Chatbot component. An\ninstance of this dictionary is used for the `metadata` field in a ChatMessage when\nthe chat message should be displayed as a thought.\nParameters:\n title: The title of the "thought" message. Required if the message is to be displayed as a thought.\n id: The ID of the message. Only used for nested thoughts. Nested thoughts can be nested by setting the parent_id to the id of the parent thought.\n parent_id: The ID of the parent message. Only used for nested thoughts.\n log: A string message to display next to the thought title in a subdued font.\n duration: The duration of the message in seconds. Appears next to the thought title in a subdued font inside a parentheses.\n status: if set to `"pending"`, a spinner appears next to the thought title and the accordion is initialized open. If `status` is `"done"`, the thought accordion is initialized closed. If `status` is not provided, the thought accordion is initialized open and no spinner is displayed.', 'properties': {'title': {'title': 'Title', 'type': 'string'}, 'id': {'anyOf': [{'type': 'integer'}, {'type': 'string'}], 'title': 'Id'}, 'parent_id': {'anyOf': [{'type': 'integer'}, {'type': 'string'}], 'title': 'Parent Id'}, 'log': {'title': 'Log', 'type': 'string'}, 'duration': {'title': 'Duration', 'type': 'number'}, 'status': {'enum': ['pending', 'done'], 'title': 'Status', 'type': 'string'}}, 'title': 'MetadataDict', 'type': 'object'}, 'OptionDict': {'description': 'A typed dictionary to represent an option in a ChatMessage. A list of these\ndictionaries is used for the `options` field in a ChatMessage.\nParameters:\n value: The value to return when the option is selected.\n label: The text to display in the option, if different from the value.', 'properties': {'value': {'title': 'Value', 'type': 'string'}, 'label': {'title': 'Label', 'type': 'string'}}, 'required': ['value'], 'title': 'OptionDict', 'type': 'object'}, 'TextMessage': {'properties': {'text': {'title': 'Text', 'type': 'string'}, 'type': {'const': 'text', 'default': 'text', 'title': 'Type', 'type': 'string'}}, 'required': ['text'], 'title': 'TextMessage', 'type': 'object'}}, 'items': {'$ref': '#/$defs/Message'}, 'title': 'ChatbotDataMessages', 'type': 'array', 'additional_description': None}, 'api_info_as_input': {'$defs': {'ComponentMessage': {'properties': {'component': {'title': 'Component', 'type': 'string'}, 'value': {'title': 'Value'}, 'constructor_args': {'additionalProperties': True, 'title': 'Constructor Args', 'type': 'object'}, 'props': {'additionalProperties': True, 'title': 'Props', 'type': 'object'}, 'type': {'const': 'component', 'default': 'component', 'title': 'Type', 'type': 'string'}}, 'required': ['component', 'value', 'constructor_args', 'props'], 'title': 'ComponentMessage', 'type': 'object'}, 'FileData': {'description': 'The FileData class is a subclass of the GradioModel class that represents a file object within a Gradio interface. It is used to store file data and metadata when a file is uploaded.\n\nAttributes:\n path: The server file path where the file is stored.\n url: The normalized server URL pointing to the file.\n size: The size of the file in bytes.\n orig_name: The original filename before upload.\n mime_type: The MIME type of the file.\n is_stream: Indicates whether the file is a stream.\n meta: Additional metadata used internally (should not be changed).', 'properties': {'path': {'title': 'Path', 'type': 'string'}, 'url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Url'}, 'size': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'title': 'Size'}, 'orig_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Orig Name'}, 'mime_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Mime Type'}, 'is_stream': {'default': False, 'title': 'Is Stream', 'type': 'boolean'}, 'meta': {'$ref': '#/$defs/FileDataMeta'}}, 'required': ['path'], 'title': 'FileData', 'type': 'object'}, 'FileDataMeta': {'properties': {'_type': {'const': 'gradio.FileData', 'title': 'Type', 'type': 'string'}}, 'required': ['_type'], 'title': 'FileDataMeta', 'type': 'object'}, 'FileMessage': {'properties': {'file': {'$ref': '#/$defs/FileData'}, 'alt_text': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Alt Text'}, 'type': {'const': 'file', 'default': 'file', 'title': 'Type', 'type': 'string'}}, 'required': ['file'], 'title': 'FileMessage', 'type': 'object'}, 'Message': {'properties': {'role': {'title': 'Role', 'type': 'string'}, 'metadata': {'anyOf': [{'$ref': '#/$defs/MetadataDict'}, {'type': 'null'}], 'default': None}, 'content': {'items': {'anyOf': [{'$ref': '#/$defs/TextMessage'}, {'$ref': '#/$defs/FileMessage'}, {'$ref': '#/$defs/ComponentMessage'}]}, 'title': 'Content', 'type': 'array'}, 'options': {'anyOf': [{'items': {'$ref': '#/$defs/OptionDict'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Options'}}, 'required': ['role', 'content'], 'title': 'Message', 'type': 'object'}, 'MetadataDict': {'description': 'A typed dictionary to represent metadata for a message in the Chatbot component. An\ninstance of this dictionary is used for the `metadata` field in a ChatMessage when\nthe chat message should be displayed as a thought.\nParameters:\n title: The title of the "thought" message. Required if the message is to be displayed as a thought.\n id: The ID of the message. Only used for nested thoughts. Nested thoughts can be nested by setting the parent_id to the id of the parent thought.\n parent_id: The ID of the parent message. Only used for nested thoughts.\n log: A string message to display next to the thought title in a subdued font.\n duration: The duration of the message in seconds. Appears next to the thought title in a subdued font inside a parentheses.\n status: if set to `"pending"`, a spinner appears next to the thought title and the accordion is initialized open. If `status` is `"done"`, the thought accordion is initialized closed. If `status` is not provided, the thought accordion is initialized open and no spinner is displayed.', 'properties': {'title': {'title': 'Title', 'type': 'string'}, 'id': {'anyOf': [{'type': 'integer'}, {'type': 'string'}], 'title': 'Id'}, 'parent_id': {'anyOf': [{'type': 'integer'}, {'type': 'string'}], 'title': 'Parent Id'}, 'log': {'title': 'Log', 'type': 'string'}, 'duration': {'title': 'Duration', 'type': 'number'}, 'status': {'enum': ['pending', 'done'], 'title': 'Status', 'type': 'string'}}, 'title': 'MetadataDict', 'type': 'object'}, 'OptionDict': {'description': 'A typed dictionary to represent an option in a ChatMessage. A list of these\ndictionaries is used for the `options` field in a ChatMessage.\nParameters:\n value: The value to return when the option is selected.\n label: The text to display in the option, if different from the value.', 'properties': {'value': {'title': 'Value', 'type': 'string'}, 'label': {'title': 'Label', 'type': 'string'}}, 'required': ['value'], 'title': 'OptionDict', 'type': 'object'}, 'TextMessage': {'properties': {'text': {'title': 'Text', 'type': 'string'}, 'type': {'const': 'text', 'default': 'text', 'title': 'Type', 'type': 'string'}}, 'required': ['text'], 'title': 'TextMessage', 'type': 'object'}}, 'items': {'$ref': '#/$defs/Message'}, 'title': 'ChatbotDataMessages', 'type': 'array', 'additional_description': None}, 'api_info_as_output': {'$defs': {'ComponentMessage': {'properties': {'component': {'title': 'Component', 'type': 'string'}, 'value': {'title': 'Value'}, 'constructor_args': {'additionalProperties': True, 'title': 'Constructor Args', 'type': 'object'}, 'props': {'additionalProperties': True, 'title': 'Props', 'type': 'object'}, 'type': {'const': 'component', 'default': 'component', 'title': 'Type', 'type': 'string'}}, 'required': ['component', 'value', 'constructor_args', 'props'], 'title': 'ComponentMessage', 'type': 'object'}, 'FileData': {'description': 'The FileData class is a subclass of the GradioModel class that represents a file object within a Gradio interface. It is used to store file data and metadata when a file is uploaded.\n\nAttributes:\n path: The server file path where the file is stored.\n url: The normalized server URL pointing to the file.\n size: The size of the file in bytes.\n orig_name: The original filename before upload.\n mime_type: The MIME type of the file.\n is_stream: Indicates whether the file is a stream.\n meta: Additional metadata used internally (should not be changed).', 'properties': {'path': {'title': 'Path', 'type': 'string'}, 'url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Url'}, 'size': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'title': 'Size'}, 'orig_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Orig Name'}, 'mime_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Mime Type'}, 'is_stream': {'default': False, 'title': 'Is Stream', 'type': 'boolean'}, 'meta': {'$ref': '#/$defs/FileDataMeta'}}, 'required': ['path'], 'title': 'FileData', 'type': 'object'}, 'FileDataMeta': {'properties': {'_type': {'const': 'gradio.FileData', 'title': 'Type', 'type': 'string'}}, 'required': ['_type'], 'title': 'FileDataMeta', 'type': 'object'}, 'FileMessage': {'properties': {'file': {'$ref': '#/$defs/FileData'}, 'alt_text': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Alt Text'}, 'type': {'const': 'file', 'default': 'file', 'title': 'Type', 'type': 'string'}}, 'required': ['file'], 'title': 'FileMessage', 'type': 'object'}, 'Message': {'properties': {'role': {'title': 'Role', 'type': 'string'}, 'metadata': {'anyOf': [{'$ref': '#/$defs/MetadataDict'}, {'type': 'null'}], 'default': None}, 'content': {'items': {'anyOf': [{'$ref': '#/$defs/TextMessage'}, {'$ref': '#/$defs/FileMessage'}, {'$ref': '#/$defs/ComponentMessage'}]}, 'title': 'Content', 'type': 'array'}, 'options': {'anyOf': [{'items': {'$ref': '#/$defs/OptionDict'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Options'}}, 'required': ['role', 'content'], 'title': 'Message', 'type': 'object'}, 'MetadataDict': {'description': 'A typed dictionary to represent metadata for a message in the Chatbot component. An\ninstance of this dictionary is used for the `metadata` field in a ChatMessage when\nthe chat message should be displayed as a thought.\nParameters:\n title: The title of the "thought" message. Required if the message is to be displayed as a thought.\n id: The ID of the message. Only used for nested thoughts. Nested thoughts can be nested by setting the parent_id to the id of the parent thought.\n parent_id: The ID of the parent message. Only used for nested thoughts.\n log: A string message to display next to the thought title in a subdued font.\n duration: The duration of the message in seconds. Appears next to the thought title in a subdued font inside a parentheses.\n status: if set to `"pending"`, a spinner appears next to the thought title and the accordion is initialized open. If `status` is `"done"`, the thought accordion is initialized closed. If `status` is not provided, the thought accordion is initialized open and no spinner is displayed.', 'properties': {'title': {'title': 'Title', 'type': 'string'}, 'id': {'anyOf': [{'type': 'integer'}, {'type': 'string'}], 'title': 'Id'}, 'parent_id': {'anyOf': [{'type': 'integer'}, {'type': 'string'}], 'title': 'Parent Id'}, 'log': {'title': 'Log', 'type': 'string'}, 'duration': {'title': 'Duration', 'type': 'number'}, 'status': {'enum': ['pending', 'done'], 'title': 'Status', 'type': 'string'}}, 'title': 'MetadataDict', 'type': 'object'}, 'OptionDict': {'description': 'A typed dictionary to represent an option in a ChatMessage. A list of these\ndictionaries is used for the `options` field in a ChatMessage.\nParameters:\n value: The value to return when the option is selected.\n label: The text to display in the option, if different from the value.', 'properties': {'value': {'title': 'Value', 'type': 'string'}, 'label': {'title': 'Label', 'type': 'string'}}, 'required': ['value'], 'title': 'OptionDict', 'type': 'object'}, 'TextMessage': {'properties': {'text': {'title': 'Text', 'type': 'string'}, 'type': {'const': 'text', 'default': 'text', 'title': 'Type', 'type': 'string'}}, 'required': ['text'], 'title': 'TextMessage', 'type': 'object'}}, 'items': {'$ref': '#/$defs/Message'}, 'title': 'ChatbotDataMessages', 'type': 'array', 'additional_description': None}, 'example_inputs': [{'role': 'user', 'metadata': None, 'content': [{'text': 'Hello!', 'type': 'text'}], 'options': None}, {'role': 'assistant', 'metadata': None, 'content': [{'text': 'How can I help you?', 'type': 'text'}], 'options': None}]}, {'id': 30, 'type': 'row', 'props': {'variant': 'default', 'visible': True, 'elem_classes': ['chat-input-row'], 'equal_height': False, 'show_progress': False, 'preserved_by_key': [], 'name': 'row'}, 'skip_api': True, 'component_class_id': '6f8a6130c432a547a95664f7f2f2a01fd8019e8e9b05282a61688092ea105a01', 'key': None}, {'id': 31, 'type': 'textbox', 'props': {'type': 'text', 'lines': 1, 'placeholder': 'Describe the clinical case or ask a follow-up question...', 'show_label': False, 'container': False, 'scale': 8, 'min_width': 160, 'visible': True, 'autofocus': False, 'autoscroll': True, 'elem_classes': [], 'preserved_by_key': ['value'], 'rtl': False, 'buttons': [], 'submit_btn': False, 'stop_btn': False, 'name': 'textbox', '_selectable': False}, 'skip_api': False, 'component_class_id': 'de54b6fd6ce8622ae36d11c1cbd43b965ca46f0c1fe283a7cec562dcb91a3208', 'key': None, 'api_info': {'type': 'string'}, 'api_info_as_input': {'type': 'string'}, 'api_info_as_output': {'type': 'string'}, 'example_inputs': 'Hello!!'}, {'id': 32, 'type': 'button', 'props': {'value': '↻', 'variant': 'secondary', 'size': 'lg', 'link_target': '_self', 'visible': True, 'interactive': True, 'elem_classes': ['btn-clear'], 'preserved_by_key': ['value'], 'scale': 0, 'min_width': 40, 'name': 'button', '_selectable': False}, 'skip_api': True, 'component_class_id': '2417a902726d3c7f260c9a2cbe2e7a1dd2fb75f94ec4fe1ce57949f9eb9b742d', 'key': None}, {'id': 33, 'type': 'button', 'props': {'value': '↑', 'variant': 'primary', 'size': 'lg', 'link_target': '_self', 'visible': True, 'interactive': True, 'elem_classes': ['btn-send'], 'preserved_by_key': ['value'], 'scale': 0, 'min_width': 40, 'name': 'button', '_selectable': False}, 'skip_api': True, 'component_class_id': '2417a902726d3c7f260c9a2cbe2e7a1dd2fb75f94ec4fe1ce57949f9eb9b742d', 'key': None}], 'css': None, 'connect_heartbeat': False, 'js': None, 'head': None, 'title': 'OncoAgent — Clinical Triage', 'space_id': None, 'enable_queue': True, 'show_error': True, 'footer_links': [], 'is_colab': False, 'max_file_size': None, 'stylesheets': [], 'theme': None, 'protocol': 'sse_v3', 'body_css': None, 'fill_height': False, 'fill_width': False, 'theme_hash': None, 'pwa': False, 'pages': [('', 'Home', True)], 'page': {'': {'layout': {'id': 0, 'children': [{'id': 1, 'children': []}, {'id': 2, 'children': []}, {'id': 3, 'children': [{'id': 4, 'children': [{'id': 5, 'children': [{'id': 6, 'children': []}, {'id': 9, 'children': [{'id': 7}, {'id': 8}]}]}, {'id': 10, 'children': [{'id': 11, 'children': [{'id': 12, 'children': []}, {'id': 13}]}, {'id': 14, 'children': [{'id': 15, 'children': []}, {'id': 16}]}]}, {'id': 17, 'children': [{'id': 18, 'children': [{'id': 19}]}, {'id': 20, 'children': [{'id': 21}]}, {'id': 22, 'children': [{'id': 23}]}]}, {'id': 24, 'children': [{'id': 25, 'children': []}, {'id': 26}]}]}, {'id': 27, 'children': [{'id': 28, 'children': [{'id': 29}, {'id': 30, 'children': [{'id': 31}, {'id': 32}, {'id': 33}]}]}]}]}]}, 'components': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33], 'dependencies': [0, 1, 2, 3]}}, 'mcp_server': False, 'i18n_translations': None, 'dependencies': [{'id': 0, 'targets': [(33, 'click')], 'inputs': [29, 31, 7, 8], 'outputs': [29, 31, 13, 16, 19, 21, 23, 26], 'backend_fn': True, 'js': None, 'queue': True, 'api_name': 'process_and_stream', 'api_description': None, 'scroll_to_output': False, 'show_progress': 'full', 'show_progress_on': None, 'batch': False, 'max_batch_size': 4, 'cancels': [], 'types': {'generator': True, 'cancel': False}, 'collects_event_data': False, 'trigger_after': None, 'trigger_only_on_success': False, 'trigger_only_on_failure': False, 'trigger_mode': 'once', 'api_visibility': 'public', 'rendered_in': None, 'render_id': None, 'connection': 'sse', 'time_limit': None, 'stream_every': 0.5, 'event_specific_args': None, 'component_prop_inputs': [], 'js_implementation': None}, {'id': 1, 'targets': [(31, 'submit')], 'inputs': [29, 31, 7, 8], 'outputs': [29, 31, 13, 16, 19, 21, 23, 26], 'backend_fn': True, 'js': None, 'queue': True, 'api_name': 'process_and_stream_1', 'api_description': None, 'scroll_to_output': False, 'show_progress': 'full', 'show_progress_on': None, 'batch': False, 'max_batch_size': 4, 'cancels': [], 'types': {'generator': True, 'cancel': False}, 'collects_event_data': False, 'trigger_after': None, 'trigger_only_on_success': False, 'trigger_only_on_failure': False, 'trigger_mode': 'once', 'api_visibility': 'public', 'rendered_in': None, 'render_id': None, 'connection': 'sse', 'time_limit': None, 'stream_every': 0.5, 'event_specific_args': None, 'component_prop_inputs': [], 'js_implementation': None}, {'id': 2, 'targets': [(32, 'click')], 'inputs': [], 'outputs': [29, 31, 7, 8, 13, 16, 19, 21, 23, 26], 'backend_fn': True, 'js': None, 'queue': True, 'api_name': 'lambda', 'api_description': None, 'scroll_to_output': False, 'show_progress': 'full', 'show_progress_on': None, 'batch': False, 'max_batch_size': 4, 'cancels': [], 'types': {'generator': False, 'cancel': False}, 'collects_event_data': False, 'trigger_after': None, 'trigger_only_on_success': False, 'trigger_only_on_failure': False, 'trigger_mode': 'once', 'api_visibility': 'public', 'rendered_in': None, 'render_id': None, 'connection': 'sse', 'time_limit': None, 'stream_every': 0.5, 'event_specific_args': None, 'component_prop_inputs': [], 'js_implementation': None}, {'id': 3, 'targets': [(None, 'load')], 'inputs': [], 'outputs': [7], 'backend_fn': True, 'js': None, 'queue': True, 'api_name': 'generate_patient_id', 'api_description': None, 'scroll_to_output': False, 'show_progress': 'full', 'show_progress_on': None, 'batch': False, 'max_batch_size': 4, 'cancels': [], 'types': {'generator': False, 'cancel': False}, 'collects_event_data': False, 'trigger_after': None, 'trigger_only_on_success': False, 'trigger_only_on_failure': False, 'trigger_mode': 'once', 'api_visibility': 'public', 'rendered_in': None, 'render_id': None, 'connection': 'sse', 'time_limit': None, 'stream_every': 0.5, 'event_specific_args': None, 'component_prop_inputs': [], 'js_implementation': None}], 'layout': {'id': 0, 'children': [{'id': 1, 'children': []}, {'id': 2, 'children': []}, {'id': 3, 'children': [{'id': 4, 'children': [{'id': 5, 'children': [{'id': 6, 'children': []}, {'id': 9, 'children': [{'id': 7}, {'id': 8}]}]}, {'id': 10, 'children': [{'id': 11, 'children': [{'id': 12, 'children': []}, {'id': 13}]}, {'id': 14, 'children': [{'id': 15, 'children': []}, {'id': 16}]}]}, {'id': 17, 'children': [{'id': 18, 'children': [{'id': 19}]}, {'id': 20, 'children': [{'id': 21}]}, {'id': 22, 'children': [{'id': 23}]}]}, {'id': 24, 'children': [{'id': 25, 'children': []}, {'id': 26}]}]}, {'id': 27, 'children': [{'id': 28, 'children': [{'id': 29}, {'id': 30, 'children': [{'id': 31}, {'id': 32}, {'id': 33}]}]}]}]}]}}
|
data/clinical_guides/esmo/ESMO_PMC10416694_Developing_a_core_set_of_patient_reported_outcomes.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:77ebd06e0a0c662b04910819cfb518b2ff10d5f4d4b578177476485e42515945
|
| 3 |
+
size 157590
|
data/clinical_guides/esmo/ESMO_PMC10664856_ESMO_ASCO_Recommendations_for_a_Global_Curriculum_.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:66c17f6d3a5409f19d8f9811b0f37b83b7c667fe171ea5ee35ab40b5fc522d24
|
| 3 |
+
size 912702
|
data/clinical_guides/esmo/ESMO_PMC10774906_ESMO_ASCO_Recommendations_for_a_Global_Curriculum_.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:951a1a53bb4b50797f5bb156acb7af7c94b4aec9cb2ee31284a787f84eae026e
|
| 3 |
+
size 1125014
|
data/clinical_guides/esmo/ESMO_PMC11574484_Effects_of_Baduanjin_exercise_on_cancer_related_fa.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:79093a78f182b14dcdd8b9c8f7ef04fd20e92ae57c3fb4b8ef93d3279db2c623
|
| 3 |
+
size 1308447
|
data/clinical_guides/esmo/ESMO_PMC11628549_Research_hotspots_and_trends_in_immunotherapy_for_.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:872d14742eab67fa0b0e57c6d5371738f090a459a66832eb3cad8bfba12a33ab
|
| 3 |
+
size 11121286
|
data/clinical_guides/esmo/ESMO_PMC11662070_Characterization_of_shared_neoantigens_landscape_i.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9a429d64c1ede46c3a5c948a72fcc9b0104aff988f1b130ed7e332732f814e2d
|
| 3 |
+
size 1661268
|
data/clinical_guides/esmo/ESMO_PMC11856526_Multiomics_in_silico_analysis_identifies_TM4SF4_as.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5a5e7d0d1b2834c0b33c56a48655cb3dd431fad755b81bfc400e889df76ed95f
|
| 3 |
+
size 4129503
|
data/clinical_guides/esmo/ESMO_PMC12218492_Plain_language_summary_of_the_THOR_Cohort_1_study_.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:66bbb6fd168c8851ec615b28eda7bfd62ab1b1d3c48eb69762a3c7cdcd071da7
|
| 3 |
+
size 1964985
|
data/clinical_guides/esmo/ESMO_PMC12306965_Systematic_critical_appraisal_of_GRADE_recommendat.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d11f20a1198835b1d7c6f28a0284fd5a904722d50a57e760bef7ce7e2008b89e
|
| 3 |
+
size 199162
|
data/clinical_guides/esmo/ESMO_PMC12381471_Prevention_and_treatment_of_venous_thromboembolism.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ffb92f9ce4d44554acfc38d0e341e2a2f93cbf961160305cc04aab149097e732
|
| 3 |
+
size 431486
|
data/clinical_guides/esmo/ESMO_PMC12733557_How_to_Read_a_Next_Generation_Sequencing_Report_fo.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d6ee5890e0fb3c71537398db23428e6284a671a35d1e4beaad945019e9ad3e99
|
| 3 |
+
size 2600162
|
data/clinical_guides/esmo/ESMO_PMC12836719_Adoption_of_electronic_patient_reported_outcomes_i.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:42fbd736ea71aa5767847068bafc22c04cfa65cf4ffba86bafe6e28a38b14d7e
|
| 3 |
+
size 521364
|
data/clinical_guides/esmo/ESMO_PMC12925129_Development_and_formative_evaluation_of_a_follow_u.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:da3cf1fd73a6ba56c436ddb96011fe91ca21825b4dc1dd8a3605e8a101acc702
|
| 3 |
+
size 663158
|
data/clinical_guides/esmo/ESMO_PMC12982907_Cost_utility_Analysis_of_R_CHOP_vs_CHOP_in_Patient.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:aa28cae3e9bd3d1a0ee96671f77007ac24815cce004032daa84efb1e7cfcc984
|
| 3 |
+
size 6726400
|
data/clinical_guides/esmo/ESMO_PMC7617288_Randomised_Trial_of_No__Short_term__or_Long_term_A.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ee8b7a0ac8377222884d97192021783710b5875caeadadd074ad2b268ad35d82
|
| 3 |
+
size 695629
|
data/clinical_guides/esmo/ESMO_PMC8267298_An_evaluation_of_the_reporting_quality_in_clinical.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1403746a915c1357ff17c694f4a40ff197e28d8b6ac07ab29b5d6b74d4ff6308
|
| 3 |
+
size 358975
|
data/clinical_guides/nccn/Patient guidelines/Guidelines for Detection, Prevention, & Risk Reduction/breastcancerscreening-patient.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:60ab6f0176fc3e6804765f489bb521bce0249023eed0b4c8124da6d0daf3c676
|
| 3 |
+
size 3082254
|
data/clinical_guides/nccn/Patient guidelines/Guidelines for Detection, Prevention, & Risk Reduction/colorectal-screening-patient.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b5a77a23f2329bee0e1859b97c74fa77a8b5ea3d16dba101b511b451d326f915
|
| 3 |
+
size 1778124
|
data/clinical_guides/nccn/Patient guidelines/Guidelines for Detection, Prevention, & Risk Reduction/genetics-patient.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a0426fa43388588f40abeb4cc6f9768d73cf410f6615f5f1678fe88d2e0fb791
|
| 3 |
+
size 3067435
|
data/clinical_guides/nccn/Patient guidelines/Guidelines for Detection, Prevention, & Risk Reduction/lung_screening-patient.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e9348bdb68b6336f80355adfdbda0ea593e9e257034c8844370aad5db891be39
|
| 3 |
+
size 1909020
|
data/clinical_guides/nccn/Patient guidelines/Guidelines for Detection, Prevention, & Risk Reduction/prostate-screening-patient.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8479e1d957db2fd12aa389ae994151d30f6e2ab6d5e1bb0d6628b464f640625b
|
| 3 |
+
size 1225976
|
data/clinical_guides/nccn/Patient guidelines/Guidelines for Specific Populations/aya-patient.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:44777805ee2d1ca752276bd8d1ebeb9cf6da92d57b65e4af2ac410493d68f4f9
|
| 3 |
+
size 1929361
|
data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/GVDH-patient-guideline.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7dcc8e5f23b17f9fb74bc8c710fc50b49756bd581cf4cc2168e6ad338034439a
|
| 3 |
+
size 1352636
|
data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/bloodclots-patient.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3cb4e08c8e6e7264b06abb7508a602764d46c034f42c4213e71f1978ce984334
|
| 3 |
+
size 3144306
|
data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/distress-patient.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:69865878db38ad4692274187bce987bf8520f102a0023cde398d4cf423e76285
|
| 3 |
+
size 694090
|
data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/fatigue-patient.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:44016e3c5b2bdfccf44d43788c726df000fa4062e81ee5c079890c635b47ac7f
|
| 3 |
+
size 696543
|
data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/immunotherapy-checkpoint-patient.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:713cffcdf1ffa176f38081b61addabb6963a24be1fa675f1d1233589978661b3
|
| 3 |
+
size 1789367
|
data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/immunotherapy-se-car-tcell-patient.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:288f6c80406f8a3eee289676ca4a4eb84d48b736dfd6fafd06c54f025586178d
|
| 3 |
+
size 1847930
|
data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/low-blood-patient.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ab9345cff6ed2c10fbd5e60f87228c579c8ddcb922c8f4151f3bbf41ff51ab6e
|
| 3 |
+
size 911272
|
data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/nausea-patient.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8287abbd7b9f7d5c6acff176aa846711fd9558e23b9e238324dc77995069036a
|
| 3 |
+
size 3485802
|
data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/palliative-patient.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5834615b9e5ac24a860d79c469757a1ea4490db788d93bb98535f424f4daab4a
|
| 3 |
+
size 1256795
|
data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/quitting-smoking-patient.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9793461e32b296e0d095e05e646aa1c5c4652ef7591f0f83433fe7b0d1cdabaf
|
| 3 |
+
size 1568449
|