File size: 12,420 Bytes
28f1212
f2c113d
 
 
 
 
 
 
 
 
 
 
3d02eb2
f2c113d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e684a6c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3d02eb2
 
 
 
 
 
 
 
 
 
13d4b74
 
 
 
e684a6c
 
 
 
 
 
 
 
 
 
 
 
f2c113d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e684a6c
 
 
 
f2c113d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e684a6c
f2c113d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# [Track A: Baseline]
"""
Domain models for the Clinical Decision Support Agent.

These Pydantic models define the structured data flowing through the agent pipeline.
Every tool consumes and produces typed models — no loose dicts or unstructured text.
"""
from __future__ import annotations

from datetime import date, datetime
from enum import Enum
from typing import List, Optional
from pydantic import BaseModel, Field, field_validator


# ──────────────────────────────────────────────
# Enums
# ──────────────────────────────────────────────

class Gender(str, Enum):
    MALE = "male"
    FEMALE = "female"
    OTHER = "other"
    UNKNOWN = "unknown"


class Severity(str, Enum):
    LOW = "low"
    MODERATE = "moderate"
    HIGH = "high"
    CRITICAL = "critical"


class Confidence(str, Enum):
    LOW = "low"
    MODERATE = "moderate"
    HIGH = "high"


class AgentStepStatus(str, Enum):
    PENDING = "pending"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"
    SKIPPED = "skipped"


# ──────────────────────────────────────────────
# Patient Data Models
# ──────────────────────────────────────────────

class Medication(BaseModel):
    name: str = Field(..., description="Medication name")
    dose: Optional[str] = Field(None, description="Dosage, e.g. '10mg daily'")
    rxcui: Optional[str] = Field(None, description="RxNorm concept ID")


class LabResult(BaseModel):
    test_name: str = Field(..., description="Lab test name")
    value: str = Field(..., description="Result value with units")
    reference_range: Optional[str] = Field(None, description="Normal reference range")
    is_abnormal: Optional[bool] = Field(None, description="Whether result is abnormal")


class VitalSigns(BaseModel):
    blood_pressure: Optional[str] = None
    heart_rate: Optional[str] = None
    temperature: Optional[str] = None
    respiratory_rate: Optional[str] = None
    oxygen_saturation: Optional[str] = None
    weight: Optional[str] = None
    height: Optional[str] = None


class PatientProfile(BaseModel):
    """Structured patient profile — output of the Patient Data Parser tool."""
    age: Optional[int] = None
    gender: Gender = Gender.UNKNOWN
    chief_complaint: str = Field(..., description="Primary reason for visit")
    history_of_present_illness: str = Field("", description="HPI narrative")
    past_medical_history: List[str] = Field(default_factory=list)
    current_medications: List[Medication] = Field(default_factory=list)
    allergies: List[str] = Field(default_factory=list)
    lab_results: List[LabResult] = Field(default_factory=list)
    vital_signs: Optional[VitalSigns] = None
    social_history: Optional[str] = None
    family_history: Optional[str] = None
    additional_notes: Optional[str] = None


# ──────────────────────────────────────────────
# Clinical Reasoning Models
# ──────────────────────────────────────────────

class DiagnosisCandidate(BaseModel):
    diagnosis: str = Field(..., description="Diagnosis name")
    icd10_code: Optional[str] = Field(None, description="ICD-10 code if known")
    likelihood: Confidence = Field(..., description="Estimated likelihood")
    supporting_evidence: List[str] = Field(default_factory=list, description="Evidence from patient data")
    reasoning: str = Field("", description="Clinical reasoning chain")


class RecommendedAction(BaseModel):
    action: str = Field(..., description="Recommended action (test, referral, treatment)")
    priority: Severity = Field(..., description="Priority level")
    rationale: str = Field("", description="Why this action is recommended")


class ClinicalReasoningResult(BaseModel):
    """Output of the Clinical Reasoning Agent (MedGemma)."""
    differential_diagnosis: List[DiagnosisCandidate] = Field(
        default_factory=list, description="Ranked differential diagnosis"
    )
    risk_assessment: Optional[str] = Field(None, description="Overall risk assessment")
    recommended_workup: List[RecommendedAction] = Field(
        default_factory=list, description="Recommended tests, referrals, treatments"
    )
    reasoning_chain: str = Field("", description="Full chain-of-thought reasoning")


# ──────────────────────────────────────────────
# Drug Interaction Models
# ──────────────────────────────────────────────

class DrugInteraction(BaseModel):
    drug_a: str
    drug_b: str
    severity: Severity
    description: str
    clinical_significance: Optional[str] = None
    source: str = Field("OpenFDA", description="Data source")


class DrugInteractionResult(BaseModel):
    """Output of the Drug Interaction Checker tool."""
    interactions_found: List[DrugInteraction] = Field(default_factory=list)
    medications_checked: List[str] = Field(default_factory=list)
    warnings: List[str] = Field(default_factory=list)


# ──────────────────────────────────────────────
# Guideline Retrieval Models
# ──────────────────────────────────────────────

class GuidelineExcerpt(BaseModel):
    title: str = Field(..., description="Guideline or source title")
    excerpt: str = Field(..., description="Relevant excerpt text")
    source: str = Field(..., description="Publication or organization")
    url: Optional[str] = None
    relevance_score: Optional[float] = None


class GuidelineRetrievalResult(BaseModel):
    """Output of the Guideline Retrieval (RAG) tool."""
    query: str = Field(..., description="The query used for retrieval")
    excerpts: List[GuidelineExcerpt] = Field(default_factory=list)


# ──────────────────────────────────────────────
# Conflict Detection Models
# ──────────────────────────────────────────────

class ConflictType(str, Enum):
    OMISSION = "omission"                # Guideline recommends X, patient not receiving X
    CONTRADICTION = "contradiction"      # Patient's current care contradicts guideline
    DOSAGE = "dosage"                    # Dose adjustment criteria apply to this patient
    MONITORING = "monitoring"            # Required monitoring not documented/ordered
    ALLERGY_RISK = "allergy_risk"        # Guideline suggests drug patient is allergic to
    INTERACTION_GAP = "interaction_gap"  # Drug interaction not addressed in current plan


class ClinicalConflict(BaseModel):
    """A single detected conflict between guidelines and patient data."""
    conflict_type: ConflictType = Field(..., description="Category of the conflict")
    severity: Severity = Field(..., description="Potential clinical impact")

    @field_validator("conflict_type", mode="before")
    @classmethod
    def _normalise_conflict_type(cls, v: str) -> str:
        return v.lower() if isinstance(v, str) else v

    @field_validator("severity", mode="before")
    @classmethod
    def _normalise_severity(cls, v: str) -> str:
        return v.lower() if isinstance(v, str) else v
    guideline_source: str = Field("", description="Which guideline flagged this")
    guideline_text: str = Field("", description="What the guideline recommends")
    patient_data: str = Field("", description="Relevant patient data that conflicts")
    description: str = Field("", description="Plain-language explanation of the gap")
    suggested_resolution: Optional[str] = Field(
        None, description="Potential resolution for the clinician to consider"
    )


class ConflictDetectionResult(BaseModel):
    """Output of the Conflict Detection tool."""
    conflicts: List[ClinicalConflict] = Field(default_factory=list)
    guidelines_checked: int = Field(0, description="Number of guidelines compared")
    summary: str = Field("", description="Brief summary of conflict analysis")


# ──────────────────────────────────────────────
# Final CDS Report
# ──────────────────────────────────────────────

class CDSReport(BaseModel):
    """
    The final Clinical Decision Support report — synthesized by MedGemma
    from all tool outputs. This is what the clinician sees.
    """
    patient_summary: str = Field(..., description="Concise patient summary")
    differential_diagnosis: List[DiagnosisCandidate] = Field(default_factory=list)
    drug_interaction_warnings: List[DrugInteraction] = Field(default_factory=list)
    guideline_recommendations: List[str] = Field(
        default_factory=list, description="Guideline-concordant recommendations"
    )
    suggested_next_steps: List[RecommendedAction] = Field(default_factory=list)
    caveats: List[str] = Field(
        default_factory=list,
        description="Limitations, uncertainties, and disclaimers"
    )
    conflicts: List[ClinicalConflict] = Field(
        default_factory=list,
        description="Detected conflicts between guidelines and patient care"
    )
    sources_cited: List[str] = Field(default_factory=list)
    generated_at: datetime = Field(default_factory=datetime.utcnow)


# ──────────────────────────────────────────────
# Agent Orchestration Models
# ──────────────────────────────────────────────

class AgentStep(BaseModel):
    """Represents a single step in the agent pipeline, streamed to the frontend."""
    step_id: str
    step_name: str
    status: AgentStepStatus = AgentStepStatus.PENDING
    tool_name: Optional[str] = None
    input_summary: Optional[str] = None
    output_summary: Optional[str] = None
    duration_ms: Optional[int] = None
    error: Optional[str] = None


class AgentState(BaseModel):
    """Full state of the agent pipeline for a given case."""
    case_id: str
    steps: List[AgentStep] = Field(default_factory=list)
    patient_profile: Optional[PatientProfile] = None
    clinical_reasoning: Optional[ClinicalReasoningResult] = None
    drug_interactions: Optional[DrugInteractionResult] = None
    guideline_retrieval: Optional[GuidelineRetrievalResult] = None
    conflict_detection: Optional[ConflictDetectionResult] = None
    final_report: Optional[CDSReport] = None
    started_at: Optional[datetime] = None
    completed_at: Optional[datetime] = None


# ──────────────────────────────────────────────
# API Request / Response Models
# ──────────────────────────────────────────────

class CaseSubmission(BaseModel):
    """API request to submit a new patient case for analysis."""
    patient_text: str = Field(
        ...,
        description="Free-text patient case description or structured data",
        min_length=10,
    )
    include_drug_check: bool = Field(True, description="Run drug interaction check")
    include_guidelines: bool = Field(True, description="Retrieve relevant guidelines")


class CaseResponse(BaseModel):
    """API response for a submitted case."""
    case_id: str
    status: str
    message: str


class CaseResult(BaseModel):
    """API response with the full case result."""
    case_id: str
    state: AgentState
    report: Optional[CDSReport] = None