import { Callout } from '../views/docs/prose';
import MermaidDiagram from '../components/MermaidDiagram';

## Appointment Reminder

This example shows how to use Guava to call patients and remind them of upcoming appointments. If a patient needs to reschedule, the agent checks your Amazon Connect queue in real time — and either transfers them directly to a live scheduling agent (if one is free) or captures their callback preference and creates a Connect task (if the queue is busy).

### What Happens on the Call

<MermaidDiagram chart={`flowchart TD
    A([Guava calls patient]) --> B{Reach the right person?}
    B -->|No| C[Leave voicemail, end call]
    B -->|Yes| D{Can they make the appointment?}
    D -->|Yes| E[Send SMS confirmation]
    D -->|No| F{Check Connect queue}
    F -->|Agent free| G[Transfer live call]
    F -->|Queue busy| H[Collect callback preference]
    H --> I[Create Connect task for scheduler callback]
`} />

### Prerequisites

- Python 3.10 or later
- A **Guava account** with an API key and a phone number — sign up at [app.goguava.ai](https://app.goguava.ai)
- An **AWS account** with an Amazon Connect instance

### Step 1: Install Guava

Choose whichever package manager you prefer:

```bash
pip install guava-sdk    # Install using pip
uv add guava-sdk         # Install using uv
poetry add guava-sdk     # Install using poetry
```

You also need `boto3` for the Amazon Connect API:

```bash
pip install boto3
```

### Step 2: Set Up Amazon Connect

If you already have a Connect instance configured, skip to the parts you haven't done yet.

#### 2a. Create a Connect Instance

1. Open the [Amazon Connect console](https://console.aws.amazon.com/connect/).
2. Click **Create instance** and follow the setup wizard.
3. Once the instance is created, open it and copy the **Instance ID** from the ARN shown in the overview — it's the UUID at the end:
   ```
   arn:aws:connect:us-east-1:123456789012:instance/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                                    This is your CONNECT_INSTANCE_ID
   ```

#### 2b. Create a Contact Flow for Task Routing

Amazon Connect tasks need a dedicated contact flow — the default queue flow does not support tasks.

1. In your Connect instance, go to **Routing → Contact flows → Create contact flow**.
2. Name it something like `Guava Task Routing Flow`.
3. Add a **Set working queue** block and connect it to your scheduling queue.
4. Add a **Transfer to queue** block after that.
5. Add a **Disconnect / hang up** block at the end of the error branch.
6. Save and publish the flow.
7. Open the flow, click **Show additional flow information**, and copy the **Contact flow ID** (the UUID at the end of the ARN).

<Callout>
**Tip:** For a minimal test flow, you can also use any existing inbound contact flow — tasks will be routed to whatever queue the flow sets.
</Callout>

#### 2c. Set Up a Scheduling Queue

1. Go to **Routing → Queues → Add new queue**.
2. Name it (e.g., `Scheduling`) and assign it to the hours of operation and outbound caller ID of your choice.
3. Copy the **Queue ID** from the queue's ARN (the UUID at the end).

#### 2d. Get a Transfer Number

This is the phone number Guava will transfer rescheduling patients to — typically your scheduling desk or the Amazon Connect direct dial number for your scheduling queue.

- In Connect, go to **Channels → Phone numbers** to see claimed numbers.
- Or use any external scheduling desk number in E.164 format (e.g., `+15551234567`).

### Step 3: Configure AWS Credentials

The example uses `boto3`, which reads credentials from the standard AWS credential chain. The easiest way for local development:

```bash
export AWS_ACCESS_KEY_ID="your-access-key-id"
export AWS_SECRET_ACCESS_KEY="your-secret-access-key"
export AWS_DEFAULT_REGION="us-east-1"   # Match your Connect instance region
```

The IAM user or role you use needs the following permissions:
- `connect:GetCurrentMetricData` — to check queue availability
- `connect:StartTaskContact` — to create callback tasks

A minimal IAM policy:
```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "connect:GetCurrentMetricData",
        "connect:StartTaskContact"
      ],
      "Resource": "arn:aws:connect:*:*:instance/YOUR_INSTANCE_ID/*"
    }
  ]
}
```

### Step 4: Set Environment Variables

```bash
# Guava
export GUAVA_API_KEY="your-guava-api-key"
export GUAVA_AGENT_NUMBER="+15551000000"    # Your Guava phone number

# Amazon Connect
export CONNECT_INSTANCE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export CONNECT_CONTACT_FLOW_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export CONNECT_QUEUE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export CONNECT_TRANSFER_NUMBER="+15551234567"   # Scheduling desk number
```

### Step 5: Run the Example

```bash
python -m examples.integrations.ccaas.amazon_connect.appointment_reminder \
  +15559876543 \
  --name "Jane Doe" \
  --appointment "Thursday, April 17 at 10:30 AM"
```

Replace `+15559876543` with the phone number to call and adjust `--name` and `--appointment` as needed.

**What to expect:**
- Guava calls the patient and verifies they're the right person
- If they confirm: you'll see a confirmation logged and an SMS is sent
- If they need to reschedule: the example checks your Connect queue in real time
  - If agents are free: Guava transfers the patient to `CONNECT_TRANSFER_NUMBER`
  - If the queue is busy: Guava collects a callback preference and creates a task in your Connect instance — visible to agents in the Contact Control Panel (CCP)
- If no one picks up: Guava leaves a voicemail

---

### How It Works

#### 1. Defining the Agent and Kicking Off the Call

```python
agent = Agent(
    name="Alex",
    organization="Bright Valley Medical Center",
    purpose="to remind patients of their upcoming appointments and assist with rescheduling when needed",
)

agent.outbound_phone(
    from_number=os.environ["GUAVA_AGENT_NUMBER"],
    to_number=args.phone,
    variables={"patient_name": args.name, "appointment": args.appointment},
).run()
```

`Agent` is the top-level handle for the AI persona. `outbound_phone` places the call and binds the agent to that single conversation. The `variables` dict is passed into the call and made available throughout — handlers can read each value with `call.get_variable(key)`.

#### 2. Reaching the Right Person (`reach_person`)

```python
@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
    call.reach_person(contact_full_name=call.get_variable("patient_name"))
```

`reach_person` handles the gatekeeper problem — it has the agent confirm they're speaking with the intended patient before proceeding. If a family member answers, the agent politely asks for the patient by name. The reach result is delivered to the handler registered with `@agent.on_reach_person`.

```python
@agent.on_reach_person
def on_reach_person(call: guava.Call, outcome: str) -> None:
    if outcome == "unavailable":
        call.hangup("Leave a brief voicemail ...")
        return
    # otherwise, set up the reminder task
```

#### 3. Collecting the Appointment Response

```python
call.set_task(
    "remind",
    objective=...,
    checklist=[
        guava.Say(f"Hi {patient_name}, this is Alex calling from ..."),
        guava.Field(
            key="response",
            description="Ask if they can make the appointment or need to reschedule.",
            field_type="text",
            required=True,
        ),
    ],
)
```

`set_task` gives the agent a named objective and a checklist of items to work through. `guava.Field` tells the agent what information to collect — the patient might say "Yes, I'll be there" or "Actually, I have a conflict that day" — and the agent normalizes that into a structured value you can inspect via `call.get_field(...)`.

#### 4. Classifying the Patient's Response

```python
from guava.helpers.openai import IntentRecognizer

intent_recognizer = IntentRecognizer(["confirmed", "needs to reschedule"])

@agent.on_task_complete("remind")
def handle_response(call: guava.Call) -> None:
    response = call.get_field("response", "")
    if intent_recognizer.classify(response) == "needs to reschedule":
        handle_reschedule(call)
    else:
        ...
```

When the `remind` task completes, the registered `@agent.on_task_complete("remind")` handler runs. Rather than matching against a list of keywords, `IntentRecognizer` uses an LLM to classify what the patient actually meant — natural phrasing like "I actually have a conflict that day" or "Can we move it?" still maps to `needs to reschedule` without enumerating every variant. The recognizer is instantiated once at module level and reused across calls.

#### 5. Checking the Connect Queue in Real Time

```python
def check_queue_availability() -> dict:
    response = connect_client.get_current_metric_data(
        InstanceId=os.environ["CONNECT_INSTANCE_ID"],
        Filters={"Queues": [os.environ["CONNECT_QUEUE_ID"]], "Channels": ["VOICE"]},
        CurrentMetrics=[
            {"Name": "AGENTS_AVAILABLE", "Unit": "COUNT"},
            {"Name": "CONTACTS_IN_QUEUE", "Unit": "COUNT"},
        ],
    )
    ...
```

`GetCurrentMetricData` returns a real-time snapshot of the queue — how many agents are available and how many contacts are already waiting. This snapshot updates every ~15 seconds.

#### 6. Routing Decision: Transfer vs. Task

```python
queue = check_queue_availability()
if queue["agents_available"] > 0:
    call.transfer(os.environ["CONNECT_TRANSFER_NUMBER"], "...")
else:
    # collect callback preference, then create a task
    call.set_task("callback", ...)
```

If a scheduler is free, Guava transfers the call live. If not, the agent collects the patient's preferred callback time in a follow-up `callback` task; its completion handler creates a Connect task via `StartTaskContact` — complete with the patient's name, original appointment, and preferred callback window. The task appears in your agents' Contact Control Panel (CCP) just like any other contact.

#### 7. SMS Confirmation

```python
call_state = {"appointment_confirmed": False}

# ... inside the "remind" handler, when the patient confirms:
call_state["appointment_confirmed"] = True

# ... after agent.run() returns:
if call_state["appointment_confirmed"]:
    guava.Client().send_sms(...)
```

After `agent.run()` returns, if the patient confirmed, we send an SMS reminder via the Guava client. This runs after the call ends, so it never blocks the voice interaction.

---

### Customization Ideas

**Pull patient data from a database or CRM**
Replace the hardcoded `--name` and `--appointment` CLI args with a database query or CRM API call, and call patients in a loop:
```python
for patient in get_todays_appointments():
    agent.outbound_phone(
        from_number=os.environ["GUAVA_AGENT_NUMBER"],
        to_number=patient["phone"],
        variables={"patient_name": patient["name"], "appointment": patient["appointment"]},
    ).run()
```

**Set a queue depth threshold**
Instead of transferring whenever any agent is free, add a threshold — e.g., only transfer if fewer than 3 contacts are already waiting:
```python
if queue["agents_available"] > 0 and queue["contacts_in_queue"] < 3:
    call.transfer(...)
```

**Route by appointment type**
Different appointment types may need different queues or transfer numbers. Pass the appointment type into the call as a variable and route accordingly:
```python
transfer_number = SPECIALIST_LINE if call.get_variable("type") == "specialist" else GENERAL_LINE
call.transfer(transfer_number, "...")
```

**Add multi-language support**
Guava supports English, Spanish, French, German, and Italian. The agent's voice can be set per call using the persona configuration on the underlying `Call` object.

### Complete Example

```python
import guava
import os
import logging
import json
import argparse
import boto3
from datetime import datetime

from guava import Agent, logging_utils
from guava.helpers.openai import IntentRecognizer

logger = logging.getLogger("appointment_reminder")

connect_client = boto3.client("connect")

intent_recognizer = IntentRecognizer(["confirmed", "needs to reschedule"])

# State the SMS step needs after the call ends.
call_state: dict = {"appointment_confirmed": False}


agent = Agent(
    name="Alex",
    organization="Bright Valley Medical Center",
    purpose=(
        "to remind patients of their upcoming appointments and assist "
        "with rescheduling when needed"
    ),
)


def check_queue_availability() -> dict:
    """Query Amazon Connect for real-time agent availability in the scheduling queue."""
    try:
        response = connect_client.get_current_metric_data(
            InstanceId=os.environ["CONNECT_INSTANCE_ID"],
            Filters={
                "Queues": [os.environ["CONNECT_QUEUE_ID"]],
                "Channels": ["VOICE"],
            },
            CurrentMetrics=[
                {"Name": "AGENTS_AVAILABLE", "Unit": "COUNT"},
                {"Name": "CONTACTS_IN_QUEUE", "Unit": "COUNT"},
            ],
        )
        agents_available = 0
        contacts_in_queue = 0
        for result in response.get("MetricResults", []):
            for collection in result.get("Collections", []):
                name = collection["Metric"]["Name"]
                value = collection.get("Value") or 0
                if name == "AGENTS_AVAILABLE":
                    agents_available = int(value)
                elif name == "CONTACTS_IN_QUEUE":
                    contacts_in_queue = int(value)
        logger.info(
            "Queue check — agents available: %d, contacts in queue: %d",
            agents_available,
            contacts_in_queue,
        )
        return {"agents_available": agents_available, "contacts_in_queue": contacts_in_queue}
    except Exception as e:
        logger.error("Failed to fetch queue metrics: %s", e)
        return {"agents_available": 0, "contacts_in_queue": 0}


@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
    call.reach_person(contact_full_name=call.get_variable("patient_name"))


@agent.on_reach_person
def on_reach_person(call: guava.Call, outcome: str) -> None:
    if outcome == "unavailable":
        appointment = call.get_variable("appointment")
        call.hangup(
            "We were unable to reach the patient. Leave a brief, friendly voicemail from "
            "Bright Valley Medical Center reminding them of their appointment on "
            f"{appointment} and asking them to call back to confirm or reschedule."
        )
        return

    patient_name = call.get_variable("patient_name")
    appointment = call.get_variable("appointment")
    call.set_task(
        "remind",
        objective=(
            f"Remind {patient_name} of their appointment at Bright Valley Medical "
            f"Center on {appointment}. Ask if they can attend or need to reschedule."
        ),
        checklist=[
            guava.Say(
                f"Hi {patient_name}, this is Alex calling from Bright Valley "
                f"Medical Center with a reminder about your appointment on {appointment}."
            ),
            guava.Field(
                key="response",
                description=(
                    "Ask if they can make the appointment or need to reschedule. "
                    "Capture their answer as 'confirmed' or 'reschedule'."
                ),
                field_type="text",
                required=True,
            ),
        ],
    )


@agent.on_task_complete("remind")
def handle_response(call: guava.Call) -> None:
    response = call.get_field("response", "")
    if intent_recognizer.classify(response) == "needs to reschedule":
        handle_reschedule(call)
    else:
        call_state["appointment_confirmed"] = True
        call.hangup(
            "Thank the patient for confirming. Remind them to arrive 15 minutes "
            "early and bring their insurance card and a photo ID. Wish them well."
        )


def handle_reschedule(call: guava.Call) -> None:
    queue = check_queue_availability()
    if queue["agents_available"] > 0:
        # Scheduling agents are free — transfer directly.
        call.transfer(
            os.environ["CONNECT_TRANSFER_NUMBER"],
            "Let the patient know you're connecting them with a scheduling specialist "
            "who will help find a new appointment time. Then transfer the call.",
        )
    else:
        # Queue is busy — collect callback preference and create a task.
        call.set_task(
            "callback",
            objective=(
                "Our scheduling team is currently busy. Collect the patient's preferred "
                "callback window so a scheduler can call them back."
            ),
            checklist=[
                guava.Say(
                    "Our scheduling team is with other patients right now. "
                    "I'll arrange for a scheduler to call you back."
                ),
                guava.Field(
                    key="preferred_callback",
                    description=(
                        "Ask when would be a good time for our scheduling team to call "
                        "them back. Capture their preferred day and time of day."
                    ),
                    field_type="text",
                    required=True,
                ),
            ],
        )


@agent.on_task_complete("callback")
def create_reschedule_task(call: guava.Call) -> None:
    patient_name = call.get_variable("patient_name")
    appointment = call.get_variable("appointment")
    preferred_callback = call.get_field("preferred_callback")

    results = {
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "agent": "Alex",
        "organization": "Bright Valley Medical Center",
        "use_case": "appointment_reminder_reschedule",
        "fields": {
            "patient_name": patient_name,
            "original_appointment": appointment,
            "preferred_callback": preferred_callback,
        },
    }
    print(json.dumps(results, indent=2))
    logger.info("Reschedule details captured.")

    try:
        connect_client.start_task_contact(
            InstanceId=os.environ["CONNECT_INSTANCE_ID"],
            ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
            Name=f"Reschedule Needed — {patient_name}"[:512],
            Description=(
                f"Patient: {patient_name}\n"
                f"Original appointment: {appointment}\n"
                f"Preferred callback: {preferred_callback}\n"
                "Action: Call patient back to reschedule their appointment."
            )[:4096],
            References={
                "source": {"Value": "guava_appointment_reminder", "Type": "STRING"},
            },
            Attributes={
                "patient_name": patient_name,
                "original_appointment": appointment,
                "preferred_callback": preferred_callback or "",
            },
        )
        logger.info("Amazon Connect reschedule task created successfully.")
    except Exception as e:
        logger.error("Failed to create Amazon Connect task: %s", e)

    call.hangup(
        "Let the patient know a scheduling specialist will call them back at their "
        "preferred time. Thank them for their patience and wish them a great day."
    )


if __name__ == "__main__":
    logging_utils.configure_logging()

    parser = argparse.ArgumentParser(
        description=(
            "Outbound appointment reminder with smart escalation for Bright Valley Medical Center. "
            "Confirms the appointment, transfers to a live scheduler if one is available, or "
            "creates an Amazon Connect callback task if the queue is busy."
        )
    )
    parser.add_argument("phone", help="Patient phone number in E.164 format (e.g. +15551234567)")
    parser.add_argument("--name", required=True, help="Full name of the patient")
    parser.add_argument(
        "--appointment",
        default="tomorrow at 9:00 AM",
        help="Appointment date and time string (default: 'tomorrow at 9:00 AM')",
    )
    args = parser.parse_args()

    logger.info(
        "Calling %s (%s) — appointment: %s",
        args.name,
        args.phone,
        args.appointment,
    )

    agent.outbound_phone(
        from_number=os.environ["GUAVA_AGENT_NUMBER"],
        to_number=args.phone,
        variables={"patient_name": args.name, "appointment": args.appointment},
    ).run()

    # Send an SMS confirmation if the patient confirmed their appointment.
    if call_state["appointment_confirmed"]:
        try:
            guava.Client().send_sms(
                from_number=os.environ["GUAVA_AGENT_NUMBER"],
                to_number=args.phone,
                message=(
                    f"Hi {args.name}, this is Bright Valley Medical Center confirming "
                    f"your appointment on {args.appointment}. Please arrive 15 minutes early "
                    "and bring your insurance card and photo ID. Reply STOP to opt out."
                ),
            )
            logger.info("SMS confirmation sent to %s.", args.phone)
        except Exception as e:
            logger.error("Failed to send SMS confirmation: %s", e)
```
