AI Symptom Checker in Mobile Applications
Symptom Checker is one of the most responsible AI tasks in mobile development. Incorrect output like "probably just fatigue" for symptoms requiring immediate care can cost lives. This isn't hyperbole—it's an architectural requirement.
Legal and Medical Constraints
Before writing code—basic constraints that dictate architecture:
- App doesn't diagnose. It outputs "probable causes," "medical consultation recommended"
- High-risk symptoms (chest pain, difficulty breathing, stroke signs)—automatic 911 redirect, no AI analysis
- Symptom data is medical data, requires HealthKit/HIPAA-level encryption
- Disclaimer visible before first query, not in small print
On iOS use HealthKit for symptom history with HKHealthStore authorization. On Android use Health Connect with HealthPermission.READ_STEPS and similar permissions.
Deterministic Critical Symptom Check
Before any LLM call—strict filter for critical symptoms:
struct CriticalSymptomChecker {
// Symptoms requiring immediate emergency care
static let emergencySymptoms = [
"chest pain", "боль в груди",
"difficulty breathing", "затруднение дыхания", "не могу дышать",
"sudden severe headache", "внезапная сильная головная боль",
"face drooping", "numbness arm", "speech difficulty", // FAST stroke test
"loss of consciousness", "потеря сознания",
"severe bleeding", "сильное кровотечение",
"choking", "подавился"
]
static func requiresEmergency(_ symptomText: String) -> Bool {
let lowercased = symptomText.lowercased()
return emergencySymptoms.contains { lowercased.contains($0) }
}
}
// Check BEFORE sending to LLM
func processSymptoms(_ userInput: String) async {
if CriticalSymptomChecker.requiresEmergency(userInput) {
showEmergencyAlert() // Button for 911, block continuation
return
}
await analyzeWithAI(userInput)
}
This isn't AI—deterministic logic. Can't trust LLM with emergency call decisions.
Structured Symptom Collection
Free text input isn't ideal UX for medical context. Stressed users write imprecisely. Better: chat with protocol-based clarifying questions.
// Symptom collection protocol (OPQRST adaptation for mobile)
enum SymptomQuestion: CaseIterable {
case onset // when started
case provocation // what intensifies/relieves
case quality // character (sharp, dull, pressing)
case radiation // where it radiates
case severity // 1-10 scale
case time // how long, continuous/intermittent
var prompt: String {
switch self {
case .onset: return "When did the symptom appear? (recently, hours ago, days ago)"
case .severity: return "Rate intensity on a scale from 1 to 10"
// ...
}
}
}
AI generates next clarifying question based on previous answers—adaptive questionnaire, not fixed list.
Symptom Analysis Prompt
func buildSymptomAnalysisPrompt(
symptoms: SymptomCollection,
patientContext: PatientContext
) -> String {
return """
You are a medical triage assistant. Analyze symptoms and suggest possible conditions.
IMPORTANT: Always recommend consulting a qualified doctor. Never provide a definitive diagnosis.
If symptoms suggest any serious condition, clearly state urgency level.
Patient context:
- Age: \(patientContext.age)
- Known conditions: \(patientContext.knownConditions.joined(separator: ", "))
- Current medications: \(patientContext.medications.isEmpty ? "none" : patientContext.medications.joined(separator: ", "))
Symptoms:
- Main complaint: \(symptoms.mainComplaint)
- Duration: \(symptoms.duration)
- Severity (1-10): \(symptoms.severity)
- Character: \(symptoms.quality)
- Associated symptoms: \(symptoms.associated.joined(separator: ", "))
Return JSON:
{
"urgency": "emergency|urgent|routine",
"possible_conditions": [{"name": "", "likelihood": "high|medium|low", "brief_explanation": ""}],
"recommended_action": "call_911|er_today|see_doctor_soon|see_doctor_routine|home_care",
"home_care_advice": "",
"red_flags": ["symptoms to watch for that require immediate care"],
"disclaimer": "This is not a medical diagnosis..."
}
"""
}
urgency: emergency in LLM response—additional trigger to show 911 button, even if deterministic filter missed it.
Result Display
Medical information requires special UI presentation:
@Composable
fun SymptomCheckResult(result: SymptomAnalysis) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
// Urgency level—first, large
UrgencyBanner(urgency = result.urgency)
Spacer(Modifier.height(16.dp))
// Recommended action—second
RecommendedActionCard(action = result.recommendedAction)
Spacer(Modifier.height(16.dp))
// Possible causes—with disclaimers
Text("Possible Causes", style = MaterialTheme.typography.titleMedium)
Text(
"Information is for informational purposes and is not a diagnosis",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
result.possibleConditions.forEach { condition ->
ConditionCard(condition)
}
// Red flags—separate section
if (result.redFlags.isNotEmpty()) {
RedFlagsSection(flags = result.redFlags)
}
// Disclaimer—mandatory, not in small print
DisclaimerCard(text = result.disclaimer)
}
}
Telemedicine Integration
If the app connects to telemedicine services, a "Consult doctor" button appears in results. Context from Symptom Checker (structured symptoms, history) passes to doctor automatically—saves 10–15 minutes of questioning.
Timeline Estimates
Basic Symptom Checker with chat, deterministic emergency filter, and AI analysis—2–3 weeks. Full implementation with adaptive questionnaire, HealthKit/Health Connect, patient profile, telemedicine integration, and GDPR/HIPAA compliance—2–3 months.







