## Outbound w/ Scheduling

In this example, we'll build an outbound voice agent for a dental office. The agent calls patients to help them schedule appointments.

### Define the Agent

`guava.Agent` is our starting point for building Guava agents. We'll create one for this example.

```python
import guava

agent = guava.Agent(
    organization="Bright Smile Dental",
    purpose="Call patients to help them schedule a dental appointment.",
)
```

### Set up DatetimeFilter

`DatetimeFilter` is a built-in helper that accepts a natural-language availability query (e.g. "Tuesdays work best") and returns a short list of matching slots from your source data. In a production system you would swap this for a call to your own scheduling backend.

```python
from guava.helpers.openai import DatetimeFilter
from guava.examples.example_data import MOCK_APPOINTMENTS

datetime_filter = DatetimeFilter(source_list=MOCK_APPOINTMENTS)
```

### Reach the right person

`on_call_start` fires at the beginning of every outbound call. We read the patient's name from the call variables and invoke `reach_person`, which instructs the agent to confirm it is speaking with the intended recipient before proceeding. Later in this example, we'll see how to set the `patient_name` variable.

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

Under the hood `reach_person()` is just a call to `set_task()`. You can replace `reach_person()` with your own [Task](https://goguava.ai/docs/tasks) if you need custom behavior here.

### Handle the reach-person outcome

`on_reach_person` fires once the agent has determined whether the intended person is available. If they are, we set a task to collect an appointment time. If not, we hang up gracefully.

```python
@agent.on_reach_person
def on_reach_person(call: guava.Call, outcome: str) -> None:
    if outcome == "unavailable":
        call.hangup("Apologize for your mistake and hang up the call.")
    elif outcome == "available":
        call.set_task(
            "schedule_appointment",
            checklist=[
                "Tell them that it's been a while since their regular cleaning with Dr. Teeth.",
                guava.Field(
                    key="appointment_time",
                    field_type="calendar_slot",
                    description="Find a time that works for the caller",
                    searchable=True,
                ),
                "Tell them their appointment has been confirmed and answer any questions before ending the call.",
            ],
        )
```

### Register an `on_search_query` handler

The `appointment_time` field has a special attribute `searchable=True` set. This turns the field into a "Search Field". Instead of providing a fixed list of choices, we will register an `on_search_query` handler.

The agent will invoke this handler to generate possible candidates for filling the field - at each invocation the agent provides a natural-language search query for us to match against.

In this example, we can simply forward that query to DatetimeFilter and return the result.

```python
@agent.on_search_query("appointment_time")
def search_appointments(call: guava.Call, query: str):
    return datetime_filter.filter(query, max_results=3)
```

### Handle task completion

`on_task_complete` fires once every item in the checklist is resolved. This is where you'd write the confirmed slot back to your database, trigger a confirmation SMS, or perform any other post-booking actions.

```python
@agent.on_task_complete("schedule_appointment")
def on_appointment_scheduled(call: guava.Call):
    call.hangup("Thank them for their time and hang up the call.")
```

### Place the outbound call

Use `call_phone` to initiate the call. Here is where you set initial values for variables - they'll be available inside your handlers via `get_variable()`.

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

# Or, test your agent in a text-based chat session in the terminal (no audio required).
agent.chat(variables={"patient_name": args.name})
```

If you intend to dial multiple participants, use [Campaigns](https://goguava.ai/docs/campaign) instead of individual outbound calls. Campaigns offer settings for automatic retries, call windows, multiple origin phone numbers, and concurrency control.

### Complete example

```python
import logging
import os
import argparse
import guava

from guava import logging_utils, Agent
from guava.examples.example_data import MOCK_APPOINTMENTS
from guava.helpers.openai import DatetimeFilter

logger = logging.getLogger("guava.examples.scheduling_outbound")

agent = Agent(
    organization="Bright Smile Dental",
    purpose="Call patients to help them schedule a dental appointment.",
)
datetime_filter = DatetimeFilter(source_list=MOCK_APPOINTMENTS)

@agent.on_call_start
def on_call_start(call: guava.Call):
    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":
        call.hangup("Apologize for your mistake and hang up the call.")
    elif outcome == "available":
        call.set_task(
            "schedule_appointment",
            checklist=[
                "Tell them that it's been a while since their regular cleaning with Dr. Teeth.",
                guava.Field(
                    key="appointment_time",
                    field_type="calendar_slot",
                    description="Find a time that works for the caller",
                    searchable=True,
                ),
                "Tell them their appointment has been confirmed and answer any questions before ending the call.",
            ],
        )

@agent.on_search_query("appointment_time")
def search_appointments(call: guava.Call, query: str):
    return datetime_filter.filter(query, max_results=3)

@agent.on_task_complete("schedule_appointment")
def on_appointment_scheduled(call: guava.Call):
    call.hangup("Thank them for their time and hang up the call.")

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

parser = argparse.ArgumentParser()
    parser.add_argument("phone", type=str, help="Phone number to call.")
    parser.add_argument("name", nargs="?", help="Name of the patient", default="Benjamin Buttons")
    args = parser.parse_args()

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