MaximoLopezChenlo commited on
Commit
e1624f5
·
verified ·
1 Parent(s): 11ba036

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.production +29 -0
  2. .gitattributes +166 -0
  3. Dockerfile +79 -0
  4. README.es.md +129 -0
  5. README.md +3 -3
  6. agents/__init__.py +32 -0
  7. agents/corrective_rag.py +353 -0
  8. agents/critic.py +290 -0
  9. agents/formatter.py +182 -0
  10. agents/graph.py +260 -0
  11. agents/memory.py +162 -0
  12. agents/nodes.py +195 -0
  13. agents/router.py +161 -0
  14. agents/specialist.py +206 -0
  15. agents/state.py +105 -0
  16. agents/tools.py +365 -0
  17. app.py +146 -37
  18. config.json +1 -0
  19. data/clinical_guides/esmo/ESMO_PMC10416694_Developing_a_core_set_of_patient_reported_outcomes.pdf +3 -0
  20. data/clinical_guides/esmo/ESMO_PMC10664856_ESMO_ASCO_Recommendations_for_a_Global_Curriculum_.pdf +3 -0
  21. data/clinical_guides/esmo/ESMO_PMC10774906_ESMO_ASCO_Recommendations_for_a_Global_Curriculum_.pdf +3 -0
  22. data/clinical_guides/esmo/ESMO_PMC11574484_Effects_of_Baduanjin_exercise_on_cancer_related_fa.pdf +3 -0
  23. data/clinical_guides/esmo/ESMO_PMC11628549_Research_hotspots_and_trends_in_immunotherapy_for_.pdf +3 -0
  24. data/clinical_guides/esmo/ESMO_PMC11662070_Characterization_of_shared_neoantigens_landscape_i.pdf +3 -0
  25. data/clinical_guides/esmo/ESMO_PMC11856526_Multiomics_in_silico_analysis_identifies_TM4SF4_as.pdf +3 -0
  26. data/clinical_guides/esmo/ESMO_PMC12218492_Plain_language_summary_of_the_THOR_Cohort_1_study_.pdf +3 -0
  27. data/clinical_guides/esmo/ESMO_PMC12306965_Systematic_critical_appraisal_of_GRADE_recommendat.pdf +3 -0
  28. data/clinical_guides/esmo/ESMO_PMC12381471_Prevention_and_treatment_of_venous_thromboembolism.pdf +3 -0
  29. data/clinical_guides/esmo/ESMO_PMC12733557_How_to_Read_a_Next_Generation_Sequencing_Report_fo.pdf +3 -0
  30. data/clinical_guides/esmo/ESMO_PMC12836719_Adoption_of_electronic_patient_reported_outcomes_i.pdf +3 -0
  31. data/clinical_guides/esmo/ESMO_PMC12925129_Development_and_formative_evaluation_of_a_follow_u.pdf +3 -0
  32. data/clinical_guides/esmo/ESMO_PMC12982907_Cost_utility_Analysis_of_R_CHOP_vs_CHOP_in_Patient.pdf +3 -0
  33. data/clinical_guides/esmo/ESMO_PMC7617288_Randomised_Trial_of_No__Short_term__or_Long_term_A.pdf +3 -0
  34. data/clinical_guides/esmo/ESMO_PMC8267298_An_evaluation_of_the_reporting_quality_in_clinical.pdf +3 -0
  35. data/clinical_guides/nccn/Patient guidelines/Guidelines for Detection, Prevention, & Risk Reduction/breastcancerscreening-patient.pdf +3 -0
  36. data/clinical_guides/nccn/Patient guidelines/Guidelines for Detection, Prevention, & Risk Reduction/colorectal-screening-patient.pdf +3 -0
  37. data/clinical_guides/nccn/Patient guidelines/Guidelines for Detection, Prevention, & Risk Reduction/genetics-patient.pdf +3 -0
  38. data/clinical_guides/nccn/Patient guidelines/Guidelines for Detection, Prevention, & Risk Reduction/lung_screening-patient.pdf +3 -0
  39. data/clinical_guides/nccn/Patient guidelines/Guidelines for Detection, Prevention, & Risk Reduction/prostate-screening-patient.pdf +3 -0
  40. data/clinical_guides/nccn/Patient guidelines/Guidelines for Specific Populations/aya-patient.pdf +3 -0
  41. data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/GVDH-patient-guideline.pdf +3 -0
  42. data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/bloodclots-patient.pdf +3 -0
  43. data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/distress-patient.pdf +3 -0
  44. data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/fatigue-patient.pdf +3 -0
  45. data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/immunotherapy-checkpoint-patient.pdf +3 -0
  46. data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/immunotherapy-se-car-tcell-patient.pdf +3 -0
  47. data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/low-blood-patient.pdf +3 -0
  48. data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/nausea-patient.pdf +3 -0
  49. data/clinical_guides/nccn/Patient guidelines/Guidelines for Supportive Care/palliative-patient.pdf +3 -0
  50. 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
+ ![ROCm](https://img.shields.io/badge/AMD-ROCm_7.2-ed1c24?logo=amd&logoColor=white)
4
+ ![Python](https://img.shields.io/badge/Python-3.10+-3776AB?logo=python&logoColor=white)
5
+ ![vLLM](https://img.shields.io/badge/vLLM-PagedAttention-000000?logo=vllm&logoColor=white)
6
+ ![LangGraph](https://img.shields.io/badge/Orchestration-LangGraph-FF4F00?logo=langchain&logoColor=white)
7
+ ![Gradio](https://img.shields.io/badge/UI-Gradio_6-FF7C00?logo=gradio&logoColor=white)
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: true
10
  license: apache-2.0
11
- short_description: Multi-agent oncology AI saving lives ¡fast cancer detection!
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
- # Header
566
- gr.HTML(HEADER_HTML)
567
- gr.HTML(INFO_HTML)
568
-
569
- # Chat
570
- chatbot = gr.Chatbot(
571
- type="messages",
572
- label="Clinical Triage Chat",
573
- height=520,
574
- show_label=False,
575
- show_copy_button=True,
576
- render_markdown=True,
577
- elem_classes=["card"],
578
- )
579
-
580
- # Controls
581
- with gr.Row():
582
- with gr.Column(scale=3):
583
- txt = gr.Textbox(
584
- placeholder="Enter a clinical case or click '▶ View Demo'...",
585
- show_label=False,
586
- lines=2,
587
- max_lines=5,
588
- )
589
- with gr.Column(scale=1, min_width=180):
590
- demo_btn = gr.Button(
591
- "▶ View Demo",
592
- elem_classes=["btn-demo"],
593
- size="lg",
594
- )
595
-
596
- with gr.Row():
597
- send_btn = gr.Button("Send", elem_classes=["btn-primary"], size="sm")
598
- clear_btn = gr.Button("🗑 Clear", variant="secondary", size="sm")
599
-
600
- # Footer
601
- gr.HTML(FOOTER_HTML)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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