File size: 8,979 Bytes
9a75c73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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
# mediagent/agents/report.py
"""
Report Agent for MediAgent.
Synthesizes outputs from Intake, Vision, and Research agents into a 
structured, professional clinical radiology report. Follows standard 
ACR/NICE radiological reporting conventions with strict JSON enforcement.
"""

import logging
from typing import Optional, Any

from core.llm import LLMClient
from core.models import IntakeOutput, ReportSection, ResearchOutput, VisionOutput

logger = logging.getLogger(__name__)


class ReportAgent:
    """
    Clinical report synthesis engine. Transforms structured agent outputs
    into a formal radiology report suitable for clinician review.
    """

    STANDARD_DISCLAIMER = (
        "This analysis is AI-generated and must be reviewed by a licensed radiologist "
        "before any clinical decisions are made."
    )

    SYSTEM_PROMPT = """You are a senior board-certified radiologist. Synthesize the provided findings into a formal radiology report. Return ONLY valid JSON:
{"clinical_history":"string","technique":"string","findings":"string","impression":"string","recommendations":"string","disclaimer":"string"}

Section rules:
1. clinical_history: Age, sex, chief complaint, relevant context — formal clinical language.
2. technique: Modality, planes/sequences, contrast status, image quality.
3. findings: Anatomical region-by-region description; severity levels (NORMAL/INCIDENTAL/SIGNIFICANT/CRITICAL); size, shape, density, location.
4. impression: Top 3 differentials with probabilities; clinical significance; end with "Confidence Level: High/Medium/Low — [reason]".
5. recommendations: Next steps based on severity and confidence (further imaging, referral, urgent eval, follow-up).
6. disclaimer: MUST be exactly: "This analysis is AI-generated and must be reviewed by a licensed radiologist before any clinical decisions are made."
No markdown, no preamble, no extra text. Use standard radiological terminology. State "Not provided" for missing data."""

    def __init__(self, llm_client: Optional[LLMClient] = None):
        self.llm = llm_client or LLMClient()

    def process(
        self,
        intake: Optional[IntakeOutput] = None,
        vision: Optional[VisionOutput] = None,
        research: Optional[ResearchOutput] = None,
    ) -> ReportSection:
        """
        Synthesize agent outputs into a formal clinical radiology report.
        
        Args:
            intake: Structured patient context and safety flags
            vision: Imaging analysis findings and technical details
            research: Ranked differential diagnoses and clinical correlations
            
        Returns:
            ReportSection: Complete, structured radiology report
        """
        logger.info("📝 Report Agent initiated report synthesis")
        
        user_prompt = self._build_user_prompt(intake, vision, research)

        result = self.llm.generate_text(
            prompt=user_prompt,
            system_prompt=self.SYSTEM_PROMPT,
            temperature=0.1,
            force_json=True,
        )

        if not result.get("success"):
            logger.error(f"❌ Report generation LLM call failed: {result.get('error')}")
            return self._get_fallback_report(intake, vision, research)

        raw_content = result.get("content", "")
        parsed = LLMClient.extract_json_from_response(raw_content)
        if not parsed:
            logger.warning("⚠️ Failed to parse report LLM JSON response. Using fallback.")
            return self._get_fallback_report(intake, vision, research)

        try:
            return self._parse_report_response(parsed)
        except Exception as e:
            logger.error(f"💥 Report mapping failed: {e}")
            return self._get_fallback_report(intake, vision, research)

    def _build_user_prompt(self, intake, vision, research) -> str:
        """Construct a highly structured prompt for the LLM synthesizer."""
        # Clinical History Block
        history = "Patient information not provided."
        if intake:
            age = f"{intake.extracted_demographics.get('age', 'Unknown')} years"
            sex = intake.extracted_demographics.get('sex', 'Unknown')
            symptoms = intake.standardized_symptoms or "No symptoms reported"
            context = intake.processing_notes or ""
            history = f"Age: {age} | Sex: {sex} | Chief Complaint: {symptoms} | Additional Context: {context}"

        # Technique Block
        technique = "Imaging technique not specified."
        if vision:
            modality = vision.modality_detected.value
            quality = vision.technical_quality
            technique = f"Modality: {modality} | Technical Assessment: {quality}"

        # Findings Block
        findings_text = "No imaging findings available."
        if vision and vision.findings:
            finding_blocks = []
            for f in vision.findings:
                finding_blocks.append(
                    f"- {f.anatomical_region}: {f.description} (Severity: {f.severity.value}, "
                    f"Confidence: {f.confidence.value} {f.confidence_score:.0f}%, Anomaly: {'Yes' if f.is_anomaly else 'No'})"
                )
            findings_text = "\n".join(finding_blocks)

        # Differentials Block
        diff_text = "No differential diagnoses generated."
        if research and research.differential_diagnoses:
            top_three = research.differential_diagnoses[:3]
            diff_blocks = []
            for d in top_three:
                diff_blocks.append(
                    f"{d.differential_rank}. {d.condition_name} (ICD-10: {d.icd10_code}) - "
                    f"Probability: {d.match_probability:.1f}% | Evidence: {d.supporting_evidence}"
                )
            diff_text = "\n".join(diff_blocks)

        return f"""Synthesize the following structured medical data into a formal radiology report:

[CLINICAL HISTORY]
{history}

[IMAGING TECHNIQUE]
{technique}

[IMAGING FINDINGS]
{findings_text}

[DIFFERENTIAL DIAGNOSES]
{diff_text}

Generate the complete report following the exact JSON schema and clinical guidelines provided in the system prompt."""

    def _parse_report_response(self, data: dict) -> ReportSection:
        """Validate and map LLM output to ReportSection model with safety enforcement."""
        # Extract fields with safe defaults
        clinical_history = str(data.get("clinical_history", "Not provided."))
        technique = str(data.get("technique", "Digital advanced imaging acquisition."))
        findings = str(data.get("findings", "No abnormalities detected."))
        impression = str(data.get("impression", "Within normal limits."))
        recommendations = str(data.get("recommendations", "Routine follow-up as clinically indicated."))
        
        # Enforce exact disclaimer for regulatory compliance
        disclaimer = self.STANDARD_DISCLAIMER

        return ReportSection(
            clinical_history=clinical_history,
            technique=technique,
            findings=findings,
            impression=impression,
            recommendations=recommendations,
            disclaimer=disclaimer
        )

    def _get_fallback_report(
        self, 
        intake: Optional[IntakeOutput], 
        vision: Optional[VisionOutput], 
        research: Optional[ResearchOutput]
    ) -> ReportSection:
        """Deterministic fallback report when LLM synthesis fails."""
        logger.warning("⚠️ Generating deterministic fallback radiology report.")
        
        # Build minimal clinically safe sections from raw data
        history = "Patient data available. See raw intake logs for details."
        if intake:
            history = f"Symptoms: {intake.standardized_symptoms} | Demographics: {intake.extracted_demographics}"
            
        technique = "Imaging modality unspecified due to processing limitation."
        if vision:
            technique = f"Modality: {vision.modality_detected.value} | Quality: {vision.technical_quality}"
            
        findings = "Imaging analysis incomplete. Manual radiologist review strongly recommended."
        if vision and vision.findings:
            findings = "; ".join([f"{f.anatomical_region}: {f.description}" for f in vision.findings])
            
        impression = "Diagnostic certainty limited. Top clinical considerations based on available data:"
        if research and research.differential_diagnoses:
            impression += " " + ", ".join([d.condition_name for d in research.differential_diagnoses[:3]])
            
        recommendations = "Urgent manual review by a licensed radiologist is required. Correlate with clinical presentation."
        
        return ReportSection(
            clinical_history=history,
            technique=technique,
            findings=findings,
            impression=impression,
            recommendations=recommendations,
            disclaimer=self.STANDARD_DISCLAIMER
        )