//! `TumorBoard` AI - Agentic Multi-Modal Medical Imaging Analysis. //! //! This demo showcases an AI system that analyzes multiple imaging modalities //! (CT, MRI, pathology) together to provide comprehensive diagnostic insights. pub mod ct_analyzer; pub mod fusion; pub mod mri_analyzer; pub mod pathology_analyzer; pub mod report_generator; pub mod sample_data; use thiserror::Error; use tumorboard_shared::{ AnalysisMetadata, CTFindings, CaseAnalysisRequest, CaseAnalysisResult, FusedAnalysis, MRIFindings, PathologyFindings, Recommendation, }; use ct_analyzer::CTAnalyzer; use fusion::MultiModalFusion; use mri_analyzer::MRIAnalyzer; use pathology_analyzer::PathologyAnalyzer; use report_generator::ReportGenerator; /// Errors that can occur during tumor board analysis. #[derive(Debug, Error)] pub enum TumorBoardError { /// No imaging data provided. #[error("No imaging data provided for analysis")] NoImagingData, /// CT analysis failed. #[error("CT analysis failed: {0}")] CTAnalysisError(String), /// MRI analysis failed. #[error("MRI analysis failed: {0}")] MRIAnalysisError(String), /// Pathology analysis failed. #[error("Pathology analysis failed: {0}")] PathologyAnalysisError(String), /// Fusion failed. #[error("Multi-modal fusion failed: {0}")] FusionError(String), /// Report generation failed. #[error("Report generation failed: {0}")] ReportError(String), } /// Main tumor board AI system. #[derive(Debug)] pub struct TumorBoardAI { ct_analyzer: CTAnalyzer, mri_analyzer: MRIAnalyzer, pathology_analyzer: PathologyAnalyzer, fusion: MultiModalFusion, report_generator: ReportGenerator, } impl Default for TumorBoardAI { fn default() -> Self { Self::new() } } impl TumorBoardAI { /// Create a new tumor board AI system. #[must_use] pub fn new() -> Self { Self { ct_analyzer: CTAnalyzer::new(), mri_analyzer: MRIAnalyzer::new(), pathology_analyzer: PathologyAnalyzer::new(), fusion: MultiModalFusion::new(), report_generator: ReportGenerator::new(), } } /// Analyze a case. /// /// # Errors /// /// Returns error if analysis fails or no imaging data is provided. pub fn analyze_case( &self, request: &CaseAnalysisRequest, ) -> Result { use std::time::Instant; let start = Instant::now(); // Check that we have at least some imaging data if request.studies.is_empty() { return Err(TumorBoardError::NoImagingData); } // Analyze each modality let ct_findings = if request.config.analyze_ct { self.analyze_ct(request)? } else { None }; let mri_findings = if request.config.analyze_mri { self.analyze_mri(request)? } else { None }; let pathology_findings = if request.config.analyze_pathology { self.analyze_pathology(request)? } else { None }; // Fuse multi-modal findings let fused = self .fusion .fuse(&ct_findings, &mri_findings, &pathology_findings) .map_err(|e| TumorBoardError::FusionError(e.to_string()))?; // Generate recommendations let recommendations = self.generate_recommendations(&fused); // Generate report if requested let report = if request.config.generate_report { Some( self.report_generator .generate( request, &ct_findings, &mri_findings, &pathology_findings, &fused, &recommendations, ) .map_err(|e| TumorBoardError::ReportError(e.to_string()))?, ) } else { None }; let processing_time = start.elapsed().as_secs_f32(); Ok(CaseAnalysisResult { case_id: request.case_id.clone(), ct_findings, mri_findings, pathology_findings, fused_analysis: fused, report, recommendations, metadata: AnalysisMetadata { processing_time, models_used: vec![ "CT-3D-CNN-v1".to_string(), "MRI-MultiSeq-v1".to_string(), "Pathology-WSI-v1".to_string(), "Fusion-CrossAttention-v1".to_string(), ], timestamp: chrono_lite_timestamp(), }, }) } /// Analyze CT studies. fn analyze_ct( &self, request: &CaseAnalysisRequest, ) -> Result, TumorBoardError> { use tumorboard_shared::ImagingModality; let ct_studies: Vec<_> = request .studies .iter() .filter(|s| s.modality == ImagingModality::CT) .collect(); if ct_studies.is_empty() { return Ok(None); } self.ct_analyzer .analyze(&ct_studies) .map(Some) .map_err(|e| TumorBoardError::CTAnalysisError(e.to_string())) } /// Analyze MRI studies. fn analyze_mri( &self, request: &CaseAnalysisRequest, ) -> Result, TumorBoardError> { use tumorboard_shared::ImagingModality; let mri_studies: Vec<_> = request .studies .iter() .filter(|s| s.modality == ImagingModality::MRI) .collect(); if mri_studies.is_empty() { return Ok(None); } self.mri_analyzer .analyze(&mri_studies) .map(Some) .map_err(|e| TumorBoardError::MRIAnalysisError(e.to_string())) } /// Analyze pathology studies. fn analyze_pathology( &self, request: &CaseAnalysisRequest, ) -> Result, TumorBoardError> { use tumorboard_shared::ImagingModality; let pathology_studies: Vec<_> = request .studies .iter() .filter(|s| s.modality == ImagingModality::Pathology) .collect(); if pathology_studies.is_empty() { return Ok(None); } self.pathology_analyzer .analyze(&pathology_studies) .map(Some) .map_err(|e| TumorBoardError::PathologyAnalysisError(e.to_string())) } /// Generate treatment recommendations based on fused analysis. fn generate_recommendations(&self, fused: &FusedAnalysis) -> Vec { use tumorboard_shared::{Priority, RecommendationType}; let mut recommendations = Vec::new(); // Recommend biopsy if high malignancy probability if fused.primary_diagnosis.probability > 0.5 { recommendations.push(Recommendation { recommendation_type: RecommendationType::Biopsy, description: "Consider tissue biopsy for definitive diagnosis".to_string(), priority: Priority::High, rationale: format!( "Primary diagnosis ({}) has {:.0}% probability", fused.primary_diagnosis.name, fused.primary_diagnosis.probability * 100.0 ), }); } // Recommend follow-up imaging if fused.confidence < 0.8 { recommendations.push(Recommendation { recommendation_type: RecommendationType::ImagingFollowUp, description: "Consider follow-up imaging for clarification".to_string(), priority: Priority::Moderate, rationale: format!( "Analysis confidence is {:.0}%, additional imaging may help", fused.confidence * 100.0 ), }); } // Recommend specialist referral for malignancy if fused.primary_diagnosis.probability > 0.7 { recommendations.push(Recommendation { recommendation_type: RecommendationType::Referral, description: "Refer to oncology for multidisciplinary evaluation".to_string(), priority: Priority::High, rationale: "High probability of malignancy requires specialist evaluation" .to_string(), }); } recommendations } /// Get a summary of findings. #[must_use] pub fn get_summary(&self, result: &CaseAnalysisResult) -> String { let mut summary = String::new(); summary.push_str(&format!("Case: {}\n", result.case_id)); summary.push('\n'); summary.push_str("PRIMARY DIAGNOSIS:\n"); summary.push_str(&format!( " {}: {:.0}% probability\n", result.fused_analysis.primary_diagnosis.name, result.fused_analysis.primary_diagnosis.probability * 100.0 )); if !result.fused_analysis.differential_diagnoses.is_empty() { summary.push_str("\nDIFFERENTIAL DIAGNOSES:\n"); for dx in &result.fused_analysis.differential_diagnoses { summary.push_str(&format!( " - {}: {:.0}%\n", dx.name, dx.probability * 100.0 )); } } if !result.recommendations.is_empty() { summary.push_str("\nRECOMMENDATIONS:\n"); for rec in &result.recommendations { summary.push_str(&format!(" [{:?}] {}\n", rec.priority, rec.description)); } } summary.push_str(&format!( "\nConfidence: {:.0}%\n", result.fused_analysis.confidence * 100.0 )); summary.push_str(&format!( "Processing time: {:.2}s\n", result.metadata.processing_time )); summary } } /// Generate a simple timestamp. fn chrono_lite_timestamp() -> String { use std::time::{SystemTime, UNIX_EPOCH}; let duration = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default(); format!("{}000", duration.as_secs()) } /// Run a complete tumor board session with sample data. /// /// # Errors /// /// Returns error if analysis fails. pub fn run_demo() -> Result { let request = tumorboard_shared::get_sample_case(); let ai = TumorBoardAI::new(); ai.analyze_case(&request) } #[cfg(test)] mod tests { use super::*; #[test] fn test_tumor_board_creation() { let ai = TumorBoardAI::new(); assert!(std::mem::size_of_val(&ai) > 0); } #[test] fn test_analyze_case() { let request = tumorboard_shared::get_sample_case(); let ai = TumorBoardAI::new(); let result = ai.analyze_case(&request); assert!(result.is_ok()); let analysis = result.unwrap(); assert!(!analysis.case_id.is_empty()); } #[test] fn test_no_imaging_data_error() { let request = CaseAnalysisRequest { case_id: "TEST001".to_string(), patient_info: tumorboard_shared::PatientInfo { age: Some(60), sex: Some(tumorboard_shared::Sex::Male), medical_history: vec![], }, studies: vec![], clinical_context: tumorboard_shared::ClinicalContext { clinical_question: "Test".to_string(), known_diagnosis: None, lab_values: vec![], prior_treatments: vec![], }, config: tumorboard_shared::AnalysisConfig::default(), }; let ai = TumorBoardAI::new(); let result = ai.analyze_case(&request); assert!(result.is_err()); assert!(matches!( result.unwrap_err(), TumorBoardError::NoImagingData )); } #[test] fn test_run_demo() { let result = run_demo(); assert!(result.is_ok()); let analysis = result.unwrap(); assert!(!analysis.fused_analysis.primary_diagnosis.name.is_empty()); } #[test] fn test_get_summary() { let request = tumorboard_shared::get_sample_case(); let ai = TumorBoardAI::new(); let result = ai.analyze_case(&request).unwrap(); let summary = ai.get_summary(&result); assert!(summary.contains("PRIMARY DIAGNOSIS")); assert!(summary.contains("Confidence")); } #[test] fn test_recommendations_generated() { let request = tumorboard_shared::get_sample_case(); let ai = TumorBoardAI::new(); let result = ai.analyze_case(&request).unwrap(); // Recommendations should be generated for significant findings assert!( !result.recommendations.is_empty() || result.fused_analysis.primary_diagnosis.probability < 0.5 ); } }