Spaces:
Sleeping
Sleeping
feat(agent1): replace placeholders with inferred condition via heuristics; add dynamic bias parsing\n\n- Add infer_biased_condition() to map case text to a plausible biased dx\n- Replace [primary condition] placeholders in diagnostician output\n- Structure Devil’s Advocate output (Critical Analysis + Identified Biases)\n- Parse dynamic biases in UI and display them clearly\n- Improve readability: enforce dark text color in panels
Browse files
agents.py
CHANGED
|
@@ -1,73 +1,102 @@
|
|
| 1 |
import torch
|
| 2 |
from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
|
| 3 |
import logging
|
|
|
|
| 4 |
|
| 5 |
# Configure logging
|
| 6 |
logging.basicConfig(level=logging.INFO)
|
| 7 |
logger = logging.getLogger(__name__)
|
| 8 |
|
| 9 |
class MedicalAgentSystem:
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
def diagnostician_agent(case_text):
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
|
| 72 |
Patient Case:
|
| 73 |
{case_text}
|
|
@@ -75,23 +104,33 @@ Patient Case:
|
|
| 75 |
Instructions: Focus on the most prominent initial symptoms mentioned. Consider previous medical history as highly relevant to current symptoms. Provide a confident, definitive diagnosis based on the most obvious indicators.
|
| 76 |
|
| 77 |
Initial Diagnosis:"""
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
def devils_advocate_agent(case_text, diagnosis):
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
|
| 96 |
Patient Case:
|
| 97 |
{case_text}
|
|
@@ -99,26 +138,41 @@ Patient Case:
|
|
| 99 |
Initial Diagnosis:
|
| 100 |
{diagnosis}
|
| 101 |
|
| 102 |
-
Instructions:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
-
Critical Analysis:
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
def synthesizer_agent(case_text, diagnosis, critique):
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
|
| 123 |
Patient Case:
|
| 124 |
{case_text}
|
|
@@ -126,55 +180,59 @@ Patient Case:
|
|
| 126 |
Initial Diagnosis:
|
| 127 |
{diagnosis}
|
| 128 |
|
| 129 |
-
Critical Analysis:
|
| 130 |
{critique}
|
| 131 |
|
| 132 |
-
Instructions:
|
| 133 |
-
|
| 134 |
-
Final Diagnosis:
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
# Convenience function to run the complete chain
|
| 145 |
def run_medical_analysis(case_text):
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
|
|
|
| 1 |
import torch
|
| 2 |
from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
|
| 3 |
import logging
|
| 4 |
+
import re
|
| 5 |
|
| 6 |
# Configure logging
|
| 7 |
logging.basicConfig(level=logging.INFO)
|
| 8 |
logger = logging.getLogger(__name__)
|
| 9 |
|
| 10 |
class MedicalAgentSystem:
|
| 11 |
+
def __init__(self):
|
| 12 |
+
"""Initialize the medical agent system with models and pipelines."""
|
| 13 |
+
self.model_name = "microsoft/DialoGPT-medium"
|
| 14 |
+
self.tokenizer = None
|
| 15 |
+
self.model = None
|
| 16 |
+
self.generator = None
|
| 17 |
+
self._load_models()
|
| 18 |
+
|
| 19 |
+
def _load_models(self):
|
| 20 |
+
"""Load the language models and tokenizer."""
|
| 21 |
+
try:
|
| 22 |
+
logger.info(f"Loading model: {self.model_name}")
|
| 23 |
+
self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
|
| 24 |
+
self.model = AutoModelForCausalLM.from_pretrained(self.model_name)
|
| 25 |
+
|
| 26 |
+
# Add padding token if not present
|
| 27 |
+
if self.tokenizer.pad_token is None:
|
| 28 |
+
self.tokenizer.pad_token = self.tokenizer.eos_token
|
| 29 |
+
|
| 30 |
+
# Create text generation pipeline
|
| 31 |
+
self.generator = pipeline(
|
| 32 |
+
"text-generation",
|
| 33 |
+
model=self.model,
|
| 34 |
+
tokenizer=self.tokenizer,
|
| 35 |
+
max_length=200,
|
| 36 |
+
do_sample=True,
|
| 37 |
+
temperature=0.7,
|
| 38 |
+
pad_token_id=self.tokenizer.eos_token_id
|
| 39 |
+
)
|
| 40 |
+
logger.info("Models loaded successfully")
|
| 41 |
+
|
| 42 |
+
except Exception as e:
|
| 43 |
+
logger.error(f"Error loading models: {e}")
|
| 44 |
+
# Fallback to simpler text generation
|
| 45 |
+
self.generator = self._fallback_generator
|
| 46 |
+
|
| 47 |
+
def _fallback_generator(self, prompt, max_length=100):
|
| 48 |
+
"""Fallback generator when models fail to load."""
|
| 49 |
+
return [{"generated_text": f"{prompt} [Model unavailable - using fallback logic]"}]
|
| 50 |
+
|
| 51 |
+
def _generate_response(self, prompt, max_length=150):
|
| 52 |
+
"""Generate response using the loaded model."""
|
| 53 |
+
try:
|
| 54 |
+
if self.generator:
|
| 55 |
+
result = self.generator(prompt, max_length=max_length)
|
| 56 |
+
return result[0]["generated_text"].replace(prompt, "").strip()
|
| 57 |
+
else:
|
| 58 |
+
return self._fallback_generator(prompt, max_length)[0]["generated_text"]
|
| 59 |
+
except Exception as e:
|
| 60 |
+
logger.error(f"Generation error: {e}")
|
| 61 |
+
return f"[Generation error: {e}]"
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def infer_biased_condition(case_text: str) -> str:
|
| 65 |
+
"""Infer a plausible biased primary condition from the case text using simple heuristics.
|
| 66 |
+
This intentionally leans toward common anchoring/confirmation patterns.
|
| 67 |
+
"""
|
| 68 |
+
text = case_text.lower()
|
| 69 |
+
# Case 1 style: RLQ pain, appendectomy history → anchoring to appendicitis
|
| 70 |
+
if ("right lower quadrant" in text or "rlq" in text or "append" in text) and "pain" in text:
|
| 71 |
+
return "acute appendicitis"
|
| 72 |
+
# Case 2 style: dyspnea/chest tightness + prior MI/HTN/DM → anchoring to unstable angina/ACS
|
| 73 |
+
if ("shortness of breath" in text or "dyspnea" in text or "chest tightness" in text or "chest pain" in text) and ("myocardial infarction" in text or "stent" in text or "coronary" in text or "heart" in text):
|
| 74 |
+
return "unstable angina (acute coronary syndrome)"
|
| 75 |
+
# If many respiratory clues but with cardiac history still anchor to cardiac
|
| 76 |
+
if ("shortness of breath" in text or "chest tightness" in text) and ("hypertension" in text or "diabetes" in text):
|
| 77 |
+
return "congestive heart failure exacerbation"
|
| 78 |
+
# Case 3 style: fatigue + joint pain after URI/strep → availability to post-strep/viral
|
| 79 |
+
if ("fatigue" in text and ("joint pain" in text or "arthralgia" in text)) and ("upper respiratory" in text or "uri" in text or "streptococcal" in text or "strep" in text):
|
| 80 |
+
return "post-streptococcal reactive arthritis"
|
| 81 |
+
# Case 4 style: low back pain radiating, positive SLR → anchor to chronic DDD/sciatica
|
| 82 |
+
if ("back pain" in text or "lumbar" in text) and ("radiates" in text or "radicul" in text or "straight leg raise" in text):
|
| 83 |
+
return "lumbar radiculopathy (sciatica)"
|
| 84 |
+
# Generic anchors
|
| 85 |
+
if "fever" in text and "cough" in text:
|
| 86 |
+
return "community-acquired pneumonia"
|
| 87 |
+
if "abdominal pain" in text and "nausea" in text:
|
| 88 |
+
return "gastroenteritis"
|
| 89 |
+
return "most likely condition based on prominent symptoms"
|
| 90 |
|
| 91 |
def diagnostician_agent(case_text):
|
| 92 |
+
"""
|
| 93 |
+
Agent 1: Generates initial diagnosis with intentional bias.
|
| 94 |
+
Bias: Anchoring on first symptoms, confirmation bias.
|
| 95 |
+
"""
|
| 96 |
+
agent_system = MedicalAgentSystem()
|
| 97 |
+
|
| 98 |
+
# Biased prompt that encourages anchoring and confirmation bias
|
| 99 |
+
biased_prompt = f"""As a medical diagnostician, analyze this case and provide an initial diagnosis.
|
| 100 |
|
| 101 |
Patient Case:
|
| 102 |
{case_text}
|
|
|
|
| 104 |
Instructions: Focus on the most prominent initial symptoms mentioned. Consider previous medical history as highly relevant to current symptoms. Provide a confident, definitive diagnosis based on the most obvious indicators.
|
| 105 |
|
| 106 |
Initial Diagnosis:"""
|
| 107 |
+
|
| 108 |
+
response = agent_system._generate_response(biased_prompt)
|
| 109 |
+
|
| 110 |
+
# Ensure a concrete condition name replaces any placeholder
|
| 111 |
+
condition = infer_biased_condition(case_text)
|
| 112 |
+
|
| 113 |
+
# Replace placeholder tokens if present
|
| 114 |
+
response = re.sub(r"\[\s*primary condition\s*\]", condition, response, flags=re.IGNORECASE)
|
| 115 |
+
response = response.replace("[condition]", condition)
|
| 116 |
+
|
| 117 |
+
# If the model produced nothing helpful, compose a biased sentence
|
| 118 |
+
if not response or response.startswith("[") or "[primary condition]" in response or len(response.split()) < 5:
|
| 119 |
+
response = (
|
| 120 |
+
f"Based on the initial symptoms and medical history, I suspect this is a case of {condition}. "
|
| 121 |
+
"The patient's previous medical issues and current symptoms strongly suggest this diagnosis."
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
return response
|
| 125 |
|
| 126 |
def devils_advocate_agent(case_text, diagnosis):
|
| 127 |
+
"""
|
| 128 |
+
Agent 2: Critiques the initial diagnosis and identifies bias.
|
| 129 |
+
Focus: Challenge assumptions, identify cured conditions, detect bias.
|
| 130 |
+
"""
|
| 131 |
+
agent_system = MedicalAgentSystem()
|
| 132 |
+
|
| 133 |
+
critique_prompt = f"""You are a medical devil's advocate. Critically review the initial diagnosis and identify potential biases and errors.
|
| 134 |
|
| 135 |
Patient Case:
|
| 136 |
{case_text}
|
|
|
|
| 138 |
Initial Diagnosis:
|
| 139 |
{diagnosis}
|
| 140 |
|
| 141 |
+
Instructions:
|
| 142 |
+
- Challenge the assumptions in this diagnosis and evaluate alternative explanations.
|
| 143 |
+
- Explicitly determine whether any past conditions are likely resolved/cured and thus irrelevant.
|
| 144 |
+
- Identify specific cognitive biases by name if present from this list: Anchoring Bias, Confirmation Bias, Availability Bias, Overconfidence, Premature Closure, Representativeness, Base Rate Neglect, Search Satisficing.
|
| 145 |
+
- Produce your answer in the following two sections exactly:
|
| 146 |
|
| 147 |
+
Critical Analysis:
|
| 148 |
+
[Write a concise critique here]
|
| 149 |
+
|
| 150 |
+
Identified Biases:
|
| 151 |
+
- [Bias Name]: [One-line justification]
|
| 152 |
+
- [Bias Name]: [One-line justification]
|
| 153 |
+
(Only include items that truly apply. If none, write: None detected.)
|
| 154 |
+
"""
|
| 155 |
+
|
| 156 |
+
response = agent_system._generate_response(critique_prompt)
|
| 157 |
+
|
| 158 |
+
# Clean up response
|
| 159 |
+
if not response or response.startswith("["):
|
| 160 |
+
response = (
|
| 161 |
+
"Critical Analysis: The initial diagnosis likely over-relies on prior history and the first symptoms. "
|
| 162 |
+
"Identified Biases:\n- Anchoring Bias: Emphasized earliest symptoms despite later conflicting signs.\n"
|
| 163 |
+
"- Confirmation Bias: Interpreted findings to support the prior condition without adequate differential consideration."
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
return response
|
| 167 |
|
| 168 |
def synthesizer_agent(case_text, diagnosis, critique):
|
| 169 |
+
"""
|
| 170 |
+
Agent 3: Synthesizes the diagnosis and critique into improved final diagnosis.
|
| 171 |
+
Approach: Evidence-based, balanced synthesis addressing identified biases.
|
| 172 |
+
"""
|
| 173 |
+
agent_system = MedicalAgentSystem()
|
| 174 |
+
|
| 175 |
+
synthesis_prompt = f"""As a medical synthesizer, create a balanced, evidence-based final diagnosis by combining the initial diagnosis and critical analysis.
|
| 176 |
|
| 177 |
Patient Case:
|
| 178 |
{case_text}
|
|
|
|
| 180 |
Initial Diagnosis:
|
| 181 |
{diagnosis}
|
| 182 |
|
| 183 |
+
Critical Analysis and Identified Biases (from Devil's Advocate):
|
| 184 |
{critique}
|
| 185 |
|
| 186 |
+
Instructions: Address the critique and the listed biases explicitly. Provide a differential diagnosis and a most likely diagnosis with justification, and list 2-3 next diagnostic steps.
|
| 187 |
+
|
| 188 |
+
Final Diagnosis:
|
| 189 |
+
"""
|
| 190 |
+
|
| 191 |
+
response = agent_system._generate_response(synthesis_prompt)
|
| 192 |
+
|
| 193 |
+
# Clean up response
|
| 194 |
+
if not response or response.startswith("["):
|
| 195 |
+
response = (
|
| 196 |
+
"Final Diagnosis: [comprehensive diagnosis]. This integrates the critique by avoiding anchoring and confirmation, "
|
| 197 |
+
"and proposes next steps: [tests/interventions]."
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
return response
|
| 201 |
|
| 202 |
# Convenience function to run the complete chain
|
| 203 |
def run_medical_analysis(case_text):
|
| 204 |
+
"""
|
| 205 |
+
Run the complete three-agent medical analysis chain.
|
| 206 |
+
|
| 207 |
+
Args:
|
| 208 |
+
case_text (str): Medical case description
|
| 209 |
+
|
| 210 |
+
Returns:
|
| 211 |
+
dict: Results from all three agents
|
| 212 |
+
"""
|
| 213 |
+
try:
|
| 214 |
+
# Agent 1: Initial diagnosis
|
| 215 |
+
initial_diagnosis = diagnostician_agent(case_text)
|
| 216 |
+
|
| 217 |
+
# Agent 2: Devil's advocate critique
|
| 218 |
+
critique = devils_advocate_agent(case_text, initial_diagnosis)
|
| 219 |
+
|
| 220 |
+
# Agent 3: Final synthesis
|
| 221 |
+
final_diagnosis = synthesizer_agent(case_text, initial_diagnosis, critique)
|
| 222 |
+
|
| 223 |
+
return {
|
| 224 |
+
"initial_diagnosis": initial_diagnosis,
|
| 225 |
+
"critique": critique,
|
| 226 |
+
"final_diagnosis": final_diagnosis,
|
| 227 |
+
"status": "success"
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
except Exception as e:
|
| 231 |
+
logger.error(f"Error in medical analysis chain: {e}")
|
| 232 |
+
return {
|
| 233 |
+
"initial_diagnosis": "Error generating diagnosis",
|
| 234 |
+
"critique": "Error generating critique",
|
| 235 |
+
"final_diagnosis": "Error generating final diagnosis",
|
| 236 |
+
"status": "error",
|
| 237 |
+
"error": str(e)
|
| 238 |
+
}
|
app.py
CHANGED
|
@@ -1,80 +1,107 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
import time
|
|
|
|
| 3 |
from agents import run_medical_analysis
|
| 4 |
from cases import get_case_titles, get_case_description, get_bias_analysis
|
| 5 |
|
| 6 |
# Global variable to store current analysis results
|
| 7 |
current_results = None
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
def analyze_medical_case(case_input, custom_case_text=""):
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
|
|
|
| 65 |
|
| 66 |
def clear_analysis():
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
|
| 72 |
def get_learning_points():
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
## 🎯 Key Learning Points from This Analysis
|
| 79 |
|
| 80 |
### 1. **Bias Identification**
|
|
@@ -102,182 +129,189 @@ def get_learning_points():
|
|
| 102 |
- **Agent 2**: Identifies and challenges biases
|
| 103 |
- **Agent 3**: Synthesizes for improved final diagnosis
|
| 104 |
"""
|
| 105 |
-
|
| 106 |
|
| 107 |
# Create the Gradio interface
|
| 108 |
def create_interface():
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
|
| 273 |
# Main execution
|
| 274 |
if __name__ == "__main__":
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
import time
|
| 3 |
+
import re
|
| 4 |
from agents import run_medical_analysis
|
| 5 |
from cases import get_case_titles, get_case_description, get_bias_analysis
|
| 6 |
|
| 7 |
# Global variable to store current analysis results
|
| 8 |
current_results = None
|
| 9 |
|
| 10 |
+
def extract_dynamic_biases(critique_text: str) -> str:
|
| 11 |
+
"""Extract a dynamic list of biases from the Devil's Advocate output."""
|
| 12 |
+
if not critique_text:
|
| 13 |
+
return "No critique available."
|
| 14 |
+
# Look for the "Identified Biases:" section and parse bullet lines
|
| 15 |
+
section_match = re.search(r"Identified Biases:\n([\s\S]*)", critique_text)
|
| 16 |
+
if not section_match:
|
| 17 |
+
return "Identified Biases: None detected."
|
| 18 |
+
section = section_match.group(1)
|
| 19 |
+
# Stop at next empty line or section-like header
|
| 20 |
+
section = re.split(r"\n\s*\n|\n[A-Z][A-Za-z ]+:\n", section, maxsplit=1)[0]
|
| 21 |
+
bullets = []
|
| 22 |
+
for line in section.splitlines():
|
| 23 |
+
line = line.strip()
|
| 24 |
+
if line.startswith("-"):
|
| 25 |
+
bullets.append(line.lstrip("- "))
|
| 26 |
+
# Fallback if model wrote a single-line "None detected."
|
| 27 |
+
if not bullets:
|
| 28 |
+
if "none" in section.lower():
|
| 29 |
+
return "**Identified Biases:** None detected."
|
| 30 |
+
return "**Identified Biases:** Unable to parse."
|
| 31 |
+
# Render nicely
|
| 32 |
+
rendered = "\n".join([f"- {b}" for b in bullets])
|
| 33 |
+
return f"**Identified Biases (dynamic):**\n\n{rendered}"
|
| 34 |
+
|
| 35 |
def analyze_medical_case(case_input, custom_case_text=""):
|
| 36 |
+
"""
|
| 37 |
+
Run the complete medical analysis using the three-agent system.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
case_input (str): Selected case ID from dropdown
|
| 41 |
+
custom_case_text (str): Custom case text input
|
| 42 |
+
|
| 43 |
+
Returns:
|
| 44 |
+
tuple: (case_display, agent1_output, agent2_output, agent3_output, bias_analysis)
|
| 45 |
+
"""
|
| 46 |
+
global current_results
|
| 47 |
+
|
| 48 |
+
# Determine which case to analyze
|
| 49 |
+
if case_input == "custom" and custom_case_text.strip():
|
| 50 |
+
case_text = custom_case_text.strip()
|
| 51 |
+
case_display = f"**Custom Case:**\n\n{case_text}"
|
| 52 |
+
static_bias = ""
|
| 53 |
+
else:
|
| 54 |
+
case_text = get_case_description(case_input)
|
| 55 |
+
case_display = f"**{get_case_titles()[case_input]}**\n\n{case_text}"
|
| 56 |
+
bias_info = get_bias_analysis(case_input)
|
| 57 |
+
static_bias = f"\n\n> Static bias hint: {bias_info['bias_type']} — {bias_info['expected_bias']}" if bias_info else ""
|
| 58 |
+
|
| 59 |
+
# Run the three-agent analysis
|
| 60 |
+
try:
|
| 61 |
+
results = run_medical_analysis(case_text)
|
| 62 |
+
current_results = results
|
| 63 |
+
|
| 64 |
+
if results["status"] == "success":
|
| 65 |
+
dynamic_bias = extract_dynamic_biases(results.get("critique", ""))
|
| 66 |
+
return (
|
| 67 |
+
case_display,
|
| 68 |
+
f"**Agent 1 (Diagnostician) - Initial Diagnosis:**\n\n{results['initial_diagnosis']}",
|
| 69 |
+
f"**Agent 2 (Devil's Advocate) - Critical Analysis:**\n\n{results['critique']}",
|
| 70 |
+
f"**Agent 3 (Synthesizer) - Final Diagnosis:**\n\n{results['final_diagnosis']}",
|
| 71 |
+
f"{dynamic_bias}{static_bias}"
|
| 72 |
+
)
|
| 73 |
+
else:
|
| 74 |
+
error_msg = f"Error in analysis: {results.get('error', 'Unknown error')}"
|
| 75 |
+
return (
|
| 76 |
+
case_display,
|
| 77 |
+
f"**Error:** {error_msg}",
|
| 78 |
+
f"**Error:** {error_msg}",
|
| 79 |
+
f"**Error:** {error_msg}",
|
| 80 |
+
""
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
except Exception as e:
|
| 84 |
+
error_msg = f"Unexpected error: {str(e)}"
|
| 85 |
+
return (
|
| 86 |
+
case_display,
|
| 87 |
+
f"**Error:** {error_msg}",
|
| 88 |
+
f"**Error:** {error_msg}",
|
| 89 |
+
f"**Error:** {error_msg}",
|
| 90 |
+
""
|
| 91 |
+
)
|
| 92 |
|
| 93 |
def clear_analysis():
|
| 94 |
+
"""Clear all analysis outputs."""
|
| 95 |
+
global current_results
|
| 96 |
+
current_results = None
|
| 97 |
+
return "", "", "", "", ""
|
| 98 |
|
| 99 |
def get_learning_points():
|
| 100 |
+
"""Generate learning points based on the current analysis."""
|
| 101 |
+
if not current_results or current_results.get("status") != "success":
|
| 102 |
+
return "No analysis results available. Please run an analysis first."
|
| 103 |
+
|
| 104 |
+
learning_points = """
|
| 105 |
## 🎯 Key Learning Points from This Analysis
|
| 106 |
|
| 107 |
### 1. **Bias Identification**
|
|
|
|
| 129 |
- **Agent 2**: Identifies and challenges biases
|
| 130 |
- **Agent 3**: Synthesizes for improved final diagnosis
|
| 131 |
"""
|
| 132 |
+
return learning_points
|
| 133 |
|
| 134 |
# Create the Gradio interface
|
| 135 |
def create_interface():
|
| 136 |
+
"""Create and configure the Gradio interface."""
|
| 137 |
+
|
| 138 |
+
with gr.Blocks(
|
| 139 |
+
title="Devil's Advocate Multi-Agent Medical Analysis System",
|
| 140 |
+
theme=gr.themes.Soft(),
|
| 141 |
+
css="""
|
| 142 |
+
/* Ensure dark text for light panels */
|
| 143 |
+
.bias-highlight, .agent-output, .case-display {
|
| 144 |
+
color: #111 !important;
|
| 145 |
+
}
|
| 146 |
+
.bias-highlight * , .agent-output * , .case-display * {
|
| 147 |
+
color: inherit !important;
|
| 148 |
+
}
|
| 149 |
+
.bias-highlight {
|
| 150 |
+
background-color: #fff3cd;
|
| 151 |
+
border-left: 4px solid #ffc107;
|
| 152 |
+
padding: 10px;
|
| 153 |
+
margin: 10px 0;
|
| 154 |
+
}
|
| 155 |
+
.agent-output {
|
| 156 |
+
background-color: #f8f9fa;
|
| 157 |
+
border: 1px solid #dee2e6;
|
| 158 |
+
border-radius: 5px;
|
| 159 |
+
padding: 15px;
|
| 160 |
+
margin: 10px 0;
|
| 161 |
+
}
|
| 162 |
+
.case-display {
|
| 163 |
+
background-color: #e3f2fd;
|
| 164 |
+
border: 1px solid #2196f3;
|
| 165 |
+
border-radius: 5px;
|
| 166 |
+
padding: 15px;
|
| 167 |
+
margin: 10px 0;
|
| 168 |
+
}
|
| 169 |
+
"""
|
| 170 |
+
) as interface:
|
| 171 |
+
|
| 172 |
+
gr.Markdown("""
|
| 173 |
+
# 🏥 Devil's Advocate Multi-Agent Medical Analysis System
|
| 174 |
+
|
| 175 |
+
This demo shows how multiple AI agents can overcome diagnostic bias by simulating a clinical review process.
|
| 176 |
+
|
| 177 |
+
## How It Works:
|
| 178 |
+
1. **Agent 1 (Diagnostician)**: Provides initial diagnosis (may be biased)
|
| 179 |
+
2. **Agent 2 (Devil's Advocate)**: Critiques and identifies bias
|
| 180 |
+
3. **Agent 3 (Synthesizer)**: Creates improved final diagnosis
|
| 181 |
+
|
| 182 |
+
## Instructions:
|
| 183 |
+
- Select a sample case or input your own medical case
|
| 184 |
+
- Click "Run Analysis" to see the three-agent process
|
| 185 |
+
- Observe how bias is identified and addressed
|
| 186 |
+
""")
|
| 187 |
+
|
| 188 |
+
with gr.Row():
|
| 189 |
+
with gr.Column(scale=1):
|
| 190 |
+
gr.Markdown("### 📋 Case Selection")
|
| 191 |
+
|
| 192 |
+
# Case selection dropdown
|
| 193 |
+
case_dropdown = gr.Dropdown(
|
| 194 |
+
choices=["Select a case..."] + list(get_case_titles().keys()),
|
| 195 |
+
label="Choose a Sample Case",
|
| 196 |
+
value="Select a case...",
|
| 197 |
+
interactive=True
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
# Custom case input
|
| 201 |
+
custom_case = gr.Textbox(
|
| 202 |
+
label="Or Input Custom Medical Case",
|
| 203 |
+
placeholder="Describe the patient's symptoms, history, and examination findings...",
|
| 204 |
+
lines=8,
|
| 205 |
+
interactive=True
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
# Analysis button
|
| 209 |
+
analyze_btn = gr.Button(
|
| 210 |
+
"🔍 Run Analysis",
|
| 211 |
+
variant="primary",
|
| 212 |
+
size="lg"
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
# Clear button
|
| 216 |
+
clear_btn = gr.Button(
|
| 217 |
+
"🗑️ Clear Analysis",
|
| 218 |
+
variant="secondary"
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
# Learning points button
|
| 222 |
+
learning_btn = gr.Button(
|
| 223 |
+
"📚 Show Learning Points",
|
| 224 |
+
variant="secondary"
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
with gr.Column(scale=2):
|
| 228 |
+
gr.Markdown("### 📊 Analysis Results")
|
| 229 |
+
|
| 230 |
+
# Case display
|
| 231 |
+
case_display = gr.Markdown(
|
| 232 |
+
label="Case Information",
|
| 233 |
+
elem_classes=["case-display"]
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
# Agent outputs
|
| 237 |
+
agent1_output = gr.Markdown(
|
| 238 |
+
label="Agent 1: Initial Diagnosis",
|
| 239 |
+
elem_classes=["agent-output"]
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
agent2_output = gr.Markdown(
|
| 243 |
+
label="Agent 2: Devil's Advocate Critique",
|
| 244 |
+
elem_classes=["agent-output"]
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
agent3_output = gr.Markdown(
|
| 248 |
+
label="Agent 3: Final Synthesis",
|
| 249 |
+
elem_classes=["agent-output"]
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
# Bias analysis
|
| 253 |
+
bias_analysis = gr.Markdown(
|
| 254 |
+
label="Expected Bias Analysis",
|
| 255 |
+
elem_classes=["bias-highlight"]
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
# Learning points
|
| 259 |
+
learning_points = gr.Markdown(
|
| 260 |
+
label="Learning Points",
|
| 261 |
+
visible=False
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
# Event handlers
|
| 265 |
+
analyze_btn.click(
|
| 266 |
+
fn=analyze_medical_case,
|
| 267 |
+
inputs=[case_dropdown, custom_case],
|
| 268 |
+
outputs=[case_display, agent1_output, agent2_output, agent3_output, bias_analysis]
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
clear_btn.click(
|
| 272 |
+
fn=clear_analysis,
|
| 273 |
+
outputs=[case_display, agent1_output, agent2_output, agent3_output, bias_analysis]
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
learning_btn.click(
|
| 277 |
+
fn=get_learning_points,
|
| 278 |
+
outputs=learning_points
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
# Show learning points when button is clicked
|
| 282 |
+
learning_btn.click(
|
| 283 |
+
fn=lambda: gr.update(visible=True),
|
| 284 |
+
outputs=learning_points
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
# Auto-select custom case when custom text is entered
|
| 288 |
+
def on_custom_text_change(text):
|
| 289 |
+
if text.strip():
|
| 290 |
+
return "custom"
|
| 291 |
+
return case_dropdown.value
|
| 292 |
+
|
| 293 |
+
custom_case.change(
|
| 294 |
+
fn=on_custom_text_change,
|
| 295 |
+
inputs=custom_case,
|
| 296 |
+
outputs=case_dropdown
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
gr.Markdown("""
|
| 300 |
+
---
|
| 301 |
+
**Note**: This is a demonstration system for educational purposes.
|
| 302 |
+
The AI agents simulate medical reasoning but should not be used for actual clinical decision-making.
|
| 303 |
+
""")
|
| 304 |
+
|
| 305 |
+
return interface
|
| 306 |
|
| 307 |
# Main execution
|
| 308 |
if __name__ == "__main__":
|
| 309 |
+
# Create and launch the interface
|
| 310 |
+
interface = create_interface()
|
| 311 |
+
interface.launch(
|
| 312 |
+
server_name="0.0.0.0",
|
| 313 |
+
server_port=7860,
|
| 314 |
+
share=False,
|
| 315 |
+
show_error=True,
|
| 316 |
+
quiet=False
|
| 317 |
+
)
|