Patient Intake AI Agents: Automating Pre-Visit Data Collection
Build an AI agent that automates patient intake by generating dynamic forms, verifying insurance eligibility, collecting medical history, and managing consent documents before the visit.
The Patient Intake Problem
The average patient spends 15 to 20 minutes filling out paperwork before every visit. Much of this data already exists somewhere in the healthcare system — previous visit records, insurance databases, pharmacy histories. An AI intake agent can pre-populate known information, ask only for what is missing, verify insurance in real time, and present everything to the clinical team before the patient walks through the door.
The result is shorter wait times, fewer data entry errors, and more complete records at the point of care.
Dynamic Form Generation
Rather than handing every patient the same 10-page clipboard, the intake agent generates forms based on the visit type, the patient's history, and what data the practice already has on file:
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
class FieldType(Enum):
TEXT = "text"
DATE = "date"
SELECT = "select"
MULTI_SELECT = "multi_select"
BOOLEAN = "boolean"
FILE_UPLOAD = "file_upload"
@dataclass
class IntakeField:
name: str
label: str
field_type: FieldType
required: bool = True
options: list[str] = field(default_factory=list)
prefilled_value: Optional[str] = None
help_text: Optional[str] = None
class IntakeFormGenerator:
def __init__(self, patient_record: dict, visit_type: str):
self.patient = patient_record
self.visit_type = visit_type
def generate_fields(self) -> list[IntakeField]:
fields = self._base_demographics()
fields += self._insurance_fields()
fields += self._visit_specific_fields()
fields += self._consent_fields()
# Remove fields we already have valid data for
return [f for f in fields if not self._is_already_complete(f)]
def _base_demographics(self) -> list[IntakeField]:
return [
IntakeField(
name="full_name",
label="Full Legal Name",
field_type=FieldType.TEXT,
prefilled_value=self.patient.get("name"),
),
IntakeField(
name="date_of_birth",
label="Date of Birth",
field_type=FieldType.DATE,
prefilled_value=self.patient.get("dob"),
),
IntakeField(
name="phone",
label="Phone Number",
field_type=FieldType.TEXT,
prefilled_value=self.patient.get("phone"),
),
IntakeField(
name="emergency_contact",
label="Emergency Contact Name and Phone",
field_type=FieldType.TEXT,
),
]
def _is_already_complete(self, field_obj: IntakeField) -> bool:
if field_obj.prefilled_value and field_obj.name != "emergency_contact":
return True
return False
The key insight is _is_already_complete — the agent skips fields where valid data already exists, turning a 40-field form into a 10-field form for returning patients.
Insurance Verification in Real Time
Instead of collecting an insurance card photo and verifying it days later, the agent verifies eligibility at intake time:
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
import httpx
from dataclasses import dataclass
from typing import Optional
@dataclass
class EligibilityResult:
is_active: bool
plan_name: str
group_number: str
copay_amount: Optional[float] = None
deductible_remaining: Optional[float] = None
requires_referral: bool = False
error_message: Optional[str] = None
class InsuranceVerifier:
def __init__(self, clearinghouse_url: str, api_key: str):
self._client = httpx.AsyncClient(
base_url=clearinghouse_url,
headers={"Authorization": f"Bearer {api_key}"},
timeout=15.0,
)
async def verify_eligibility(
self, payer_id: str, member_id: str, dob: str, service_type: str
) -> EligibilityResult:
payload = {
"payer_id": payer_id,
"member_id": member_id,
"date_of_birth": dob,
"service_type_code": service_type,
"date_of_service": "2026-03-17",
}
try:
response = await self._client.post("/eligibility/verify", json=payload)
response.raise_for_status()
data = response.json()
return EligibilityResult(
is_active=data["active"],
plan_name=data["plan_name"],
group_number=data["group_number"],
copay_amount=data.get("copay"),
deductible_remaining=data.get("deductible_remaining"),
requires_referral=data.get("referral_required", False),
)
except httpx.HTTPStatusError as e:
return EligibilityResult(
is_active=False,
plan_name="",
group_number="",
error_message=f"Verification failed: {e.response.status_code}",
)
Medical History Collection
The agent collects medical history conversationally, asking follow-up questions based on previous answers:
class MedicalHistoryCollector:
CONDITION_FOLLOWUPS = {
"diabetes": ["What type of diabetes?", "Current medications?", "Last A1C level?"],
"hypertension": ["Current blood pressure medications?", "Last BP reading?"],
"asthma": ["Current inhalers?", "Frequency of attacks?"],
}
def __init__(self):
self.collected: dict = {}
self.pending_followups: list[str] = []
def process_conditions(self, conditions: list[str]) -> list[str]:
followup_questions = []
for condition in conditions:
self.collected[condition] = {"reported": True}
normalized = condition.lower().strip()
if normalized in self.CONDITION_FOLLOWUPS:
followup_questions.extend(self.CONDITION_FOLLOWUPS[normalized])
self.pending_followups = followup_questions
return followup_questions
def get_summary(self) -> dict:
return {
"conditions": self.collected,
"complete": len(self.pending_followups) == 0,
"pending_questions": self.pending_followups,
}
Consent Management
Digital consent must be explicit, timestamped, and auditable:
from datetime import datetime
import hashlib
@dataclass
class ConsentRecord:
patient_id: str
consent_type: str
version: str
granted: bool
timestamp: datetime
ip_address: str
document_hash: str
class ConsentManager:
REQUIRED_CONSENTS = [
("treatment", "Consent to Treatment", "v2.1"),
("privacy", "Notice of Privacy Practices", "v3.0"),
("telehealth", "Telehealth Consent", "v1.4"),
]
def get_pending_consents(self, patient_id: str, existing: list[str]) -> list[tuple]:
return [
(ctype, label, version)
for ctype, label, version in self.REQUIRED_CONSENTS
if ctype not in existing
]
def record_consent(
self, patient_id: str, consent_type: str, version: str, document_text: str, ip: str
) -> ConsentRecord:
doc_hash = hashlib.sha256(document_text.encode()).hexdigest()
return ConsentRecord(
patient_id=patient_id,
consent_type=consent_type,
version=version,
granted=True,
timestamp=datetime.utcnow(),
ip_address=ip,
document_hash=doc_hash,
)
FAQ
How does the agent handle patients who cannot complete digital intake?
The agent should detect when a patient is struggling (repeated validation errors, long pauses, requests for help) and offer to transfer to a staff member. The partially completed data is saved so the staff member can pick up where the patient left off rather than starting over.
What happens when insurance verification returns conflicting data?
The agent flags discrepancies — such as a member ID that matches but the name does not — and escalates to billing staff. It does not silently accept mismatched data, as this leads to claim denials downstream.
Can the intake agent handle minors and dependents?
Yes, but with additional logic. For patients under 18, the agent must collect the legal guardian's information and obtain consent from the guardian rather than the patient. The form generator checks the patient's date of birth and adjusts the required fields accordingly.
#HealthcareAI #PatientIntake #InsuranceVerification #MedicalHistory #Python #AgenticAI #LearnAI #AIEngineering
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.