Skip to content
Learn Agentic AI14 min read0 views

Building a Post-Operative Care Agent: Recovery Instructions and Follow-Up Scheduling

Build an AI agent that delivers personalized post-operative care instructions, monitors patient recovery through symptom check-ins, triggers clinical alerts when needed, and schedules follow-up appointments automatically.

The Gap Between Discharge and Recovery

After a dental procedure, patients leave with a printed instruction sheet they often lose before reaching their car. Questions arise at night and on weekends when the office is closed. A post-operative care agent fills this gap by delivering instructions at the right time, checking in on recovery milestones, and escalating to the clinical team when symptoms suggest complications.

Post-Op Instruction Engine

The instruction engine maps each procedure type to a timeline of care instructions. Instead of dumping all information at once, it delivers relevant guidance at each stage of recovery.

from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Optional
from enum import Enum


class RecoveryPhase(Enum):
    IMMEDIATE = "immediate"       # 0-2 hours
    FIRST_DAY = "first_day"       # 2-24 hours
    EARLY_RECOVERY = "early"      # 1-3 days
    MID_RECOVERY = "mid"          # 3-7 days
    LATE_RECOVERY = "late"        # 7-14 days


@dataclass
class CareInstruction:
    phase: RecoveryPhase
    title: str
    instructions: list[str]
    warnings: list[str] = field(default_factory=list)
    send_at_offset_hours: int = 0


EXTRACTION_INSTRUCTIONS = [
    CareInstruction(
        phase=RecoveryPhase.IMMEDIATE,
        title="Right After Your Extraction",
        instructions=[
            "Keep biting on the gauze for 30-45 minutes.",
            "Do not spit, use a straw, or rinse your mouth.",
            "Apply an ice pack to your cheek: 20 minutes "
            "on, 20 minutes off.",
            "Take prescribed pain medication before the "
            "numbness wears off.",
        ],
        warnings=[
            "Some bleeding is normal. If bleeding does not "
            "slow after 2 hours of steady gauze pressure, "
            "call the office.",
        ],
        send_at_offset_hours=0,
    ),
    CareInstruction(
        phase=RecoveryPhase.FIRST_DAY,
        title="First 24 Hours",
        instructions=[
            "Eat soft foods: yogurt, mashed potatoes, soup.",
            "Do not smoke or use tobacco products.",
            "Sleep with your head elevated on an extra pillow.",
            "Take ibuprofen 400mg every 6 hours for pain.",
        ],
        warnings=[
            "Call us if you develop a fever above 101F.",
        ],
        send_at_offset_hours=4,
    ),
    CareInstruction(
        phase=RecoveryPhase.EARLY_RECOVERY,
        title="Days 2-3 Recovery Check",
        instructions=[
            "Gently rinse with warm salt water after meals.",
            "You can begin eating slightly firmer foods.",
            "Continue taking medication as prescribed.",
            "Swelling should start to decrease.",
        ],
        warnings=[
            "Increasing pain after day 3 could indicate "
            "dry socket. Contact us if pain suddenly "
            "worsens.",
        ],
        send_at_offset_hours=48,
    ),
    CareInstruction(
        phase=RecoveryPhase.MID_RECOVERY,
        title="One Week Check-In",
        instructions=[
            "Resume normal brushing, being gentle near "
            "the extraction site.",
            "Most discomfort should be gone by now.",
            "Resume normal diet as comfort allows.",
        ],
        send_at_offset_hours=168,
    ),
]

PROCEDURE_INSTRUCTIONS = {
    "extraction": EXTRACTION_INSTRUCTIONS,
    "root_canal": [],   # similar structure omitted
    "implant": [],      # similar structure omitted
    "crown": [],        # similar structure omitted
}

Symptom Monitoring and Check-In System

The agent sends scheduled check-in messages that ask the patient to report their symptoms. It uses structured questions to gather quantifiable data.

@dataclass
class SymptomCheckIn:
    question: str
    response_type: str  # "scale_1_10", "yes_no", "free_text"
    alert_threshold: Optional[int] = None
    follow_up_question: Optional[str] = None


CHECK_IN_QUESTIONS = {
    RecoveryPhase.FIRST_DAY: [
        SymptomCheckIn(
            "On a scale of 1-10, how is your pain level?",
            "scale_1_10",
            alert_threshold=8,
        ),
        SymptomCheckIn(
            "Is the bleeding controlled?",
            "yes_no",
            follow_up_question=(
                "Is it steady oozing or active bleeding?"
            ),
        ),
        SymptomCheckIn(
            "Have you been able to take your medication?",
            "yes_no",
        ),
    ],
    RecoveryPhase.EARLY_RECOVERY: [
        SymptomCheckIn(
            "How is your pain compared to yesterday? "
            "(1=much better, 5=same, 10=much worse)",
            "scale_1_10",
            alert_threshold=7,
        ),
        SymptomCheckIn(
            "Do you have any swelling?",
            "yes_no",
            follow_up_question="Is it increasing or stable?",
        ),
        SymptomCheckIn(
            "Are you experiencing any unusual taste "
            "or bad odor?",
            "yes_no",
            alert_threshold=1,  # any yes triggers review
        ),
    ],
}


class SymptomMonitor:
    def __init__(self, db, sms_client, alert_service):
        self.db = db
        self.sms = sms_client
        self.alerts = alert_service

    async def send_check_in(
        self, patient_id: str, procedure_id: str,
        phase: RecoveryPhase,
    ):
        questions = CHECK_IN_QUESTIONS.get(phase, [])
        if not questions:
            return

        patient = await self.db.fetchrow(
            "SELECT * FROM patients WHERE id = $1",
            patient_id,
        )

        for q in questions:
            await self.sms.send(
                patient["phone"], q.question
            )
            await self.db.execute("""
                INSERT INTO symptom_check_ins
                    (patient_id, procedure_id, phase,
                     question, sent_at)
                VALUES ($1, $2, $3, $4, $5)
            """, patient_id, procedure_id, phase.value,
                 q.question, datetime.utcnow())

    async def process_response(
        self, patient_id: str, response_text: str,
    ):
        latest_question = await self.db.fetchrow("""
            SELECT * FROM symptom_check_ins
            WHERE patient_id = $1
              AND response IS NULL
            ORDER BY sent_at DESC LIMIT 1
        """, patient_id)

        if not latest_question:
            return

        question_def = self._find_question_def(
            latest_question["phase"],
            latest_question["question"],
        )

        parsed_value = self._parse_response(
            response_text, question_def.response_type
        )

        await self.db.execute("""
            UPDATE symptom_check_ins
            SET response = $2, responded_at = $3
            WHERE id = $1
        """, latest_question["id"], str(parsed_value),
             datetime.utcnow())

        if self._should_alert(question_def, parsed_value):
            await self.alerts.notify_clinical_team(
                patient_id=patient_id,
                alert_type="symptom_concern",
                details=(
                    f"Patient reported {parsed_value} for: "
                    f"{question_def.question}"
                ),
            )

    def _parse_response(self, text, response_type):
        if response_type == "scale_1_10":
            import re
            numbers = re.findall(r"\d+", text)
            return int(numbers[0]) if numbers else 5
        elif response_type == "yes_no":
            lower = text.lower().strip()
            return 1 if lower in ("yes", "y", "yeah") else 0
        return text

    def _should_alert(self, question_def, value):
        if question_def.alert_threshold is None:
            return False
        return int(value) >= question_def.alert_threshold

    def _find_question_def(self, phase, question_text):
        phase_enum = RecoveryPhase(phase)
        for q in CHECK_IN_QUESTIONS.get(phase_enum, []):
            if q.question == question_text:
                return q
        return SymptomCheckIn(question_text, "free_text")

Alert Trigger System

When symptom responses exceed thresholds, the agent escalates to the clinical team through the appropriate channel based on severity.

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

class AlertSeverity(Enum):
    LOW = "low"         # log only
    MEDIUM = "medium"   # notification to provider
    HIGH = "high"       # immediate page
    CRITICAL = "critical"  # emergency protocol


class ClinicalAlertService:
    def __init__(self, db, pager, notification_svc):
        self.db = db
        self.pager = pager
        self.notify = notification_svc

    async def notify_clinical_team(
        self, patient_id: str, alert_type: str,
        details: str,
    ):
        severity = self._assess_severity(
            alert_type, details
        )

        await self.db.execute("""
            INSERT INTO clinical_alerts
                (patient_id, alert_type, severity,
                 details, created_at, resolved)
            VALUES ($1, $2, $3, $4, $5, false)
        """, patient_id, alert_type, severity.value,
             details, datetime.utcnow())

        if severity == AlertSeverity.HIGH:
            provider = await self._get_treating_provider(
                patient_id
            )
            await self.pager.send(
                provider["phone"],
                f"POST-OP ALERT: {details}",
            )
        elif severity == AlertSeverity.CRITICAL:
            await self.pager.send_emergency(
                patient_id, details
            )

    def _assess_severity(self, alert_type, details):
        if "fever" in details.lower():
            return AlertSeverity.HIGH
        if "bleeding" in details.lower():
            return AlertSeverity.HIGH
        if "increasing pain" in details.lower():
            return AlertSeverity.MEDIUM
        return AlertSeverity.LOW

Follow-Up Appointment Auto-Scheduling

The agent automatically schedules follow-up appointments based on the procedure type and recovery timeline.

FOLLOW_UP_RULES = {
    "extraction": {"days_after": 7, "type": "post_op_check"},
    "root_canal": {"days_after": 14, "type": "crown_consult"},
    "implant": {"days_after": 10, "type": "implant_check"},
}


class FollowUpScheduler:
    def __init__(self, db, schedule_manager):
        self.db = db
        self.scheduler = schedule_manager

    async def schedule_follow_up(
        self, patient_id: str, procedure_type: str,
        procedure_date: datetime, provider_id: str,
    ):
        rule = FOLLOW_UP_RULES.get(procedure_type)
        if not rule:
            return None

        target_date = (
            procedure_date + timedelta(days=rule["days_after"])
        ).date()

        slots = await self.scheduler.find_available_slots(
            appointment_type=rule["type"],
            preferred_date=target_date,
            provider_id=provider_id,
        )

        if slots:
            appointment = await self.scheduler.book_appointment(
                patient_id=patient_id,
                slot=slots[0],
                appointment_type=rule["type"],
            )
            return appointment
        return None

FAQ

How does the agent know when to escalate versus when to reassure the patient?

The alert system uses a combination of threshold-based rules and trend analysis. A single high pain score triggers a notification, but the system also watches for trends — pain that increases day over day even if below the absolute threshold still gets flagged. The clinical team defines the thresholds per procedure type, and the system never provides medical advice beyond the pre-approved care instructions.

What if the patient does not respond to a symptom check-in?

If a patient misses a check-in, the agent sends one follow-up message two hours later. If there is still no response, the front desk receives a task to call the patient directly. The system never assumes that silence means everything is fine — a non-response after a surgical procedure is treated as a reason for human follow-up.

Can the post-op agent handle multiple simultaneous procedures, such as multiple extractions done in one visit?

Yes. Each procedure creates its own recovery timeline, but the agent consolidates messages so the patient receives one check-in that covers all procedures rather than separate messages for each tooth. The most conservative recovery instructions take precedence — for example, if one extraction was surgical and one was simple, the surgical recovery guidelines apply to the overall care plan.


#PostOpCare #RecoveryMonitoring #HealthcareAI #PatientFollowUp #Python #AgenticAI #LearnAI #AIEngineering

Share this article
C

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.