## 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
export const AGENT_PY = `import guava

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

```typescript
export const AGENT_TS = `import * as guava from "@guava-ai/guava-sdk";

const agent = new guava.Agent({
  organization: "Bright Smile Dental",
  purpose: "You are calling 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
export const FILTER_PY = `from guava.helpers.openai import DatetimeFilter
from guava.examples.example_data import MOCK_APPOINTMENTS

datetime_filter = DatetimeFilter(source_list=MOCK_APPOINTMENTS)`;
```

```typescript
export const FILTER_TS = `import { DatetimeFilter } from "@guava-ai/guava-sdk/helpers/openai";
import { mockAppointmentsForFuture } from "@guava-ai/guava-sdk/example-data";

const datetimeFilter = new DatetimeFilter({
  sourceList: mockAppointmentsForFuture(),
});`;
```

### 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
export const START_PY = `@agent.on_call_start
def on_call_start(call: guava.Call):
    call.reach_person(
        contact_full_name=call.get_variable("patient_name"),
    )`;
```

```typescript
export const START_TS = `agent.onCallStart(async (call: guava.Call) => {
  await call.reachPerson(await call.getVariable("patientName"));
});`;
```

### 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
export const REACH_PY = `@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.",
            ],
        )`;
```

```typescript
export const REACH_TS = `agent.onReachPerson(async (call: guava.Call, outcome: string) => {
  if (outcome === "available") {
    await call.setTask({
      taskId: "schedule_appointment",
      checklist: [
        "Tell them that it's been a while since their regular cleaning with Dr. Teeth.",
        guava.Field({
          key: "appointment_time",
          fieldType: "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.",
      ],
    });
  } else {
    await call.hangup("Apologize for your mistake and hang up 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
export const SEARCH_PY = `@agent.on_search_query("appointment_time")
def search_appointments(call: guava.Call, query: str):
    return datetime_filter.filter(query, max_results=3)`;
```

```typescript
export const SEARCH_TS = `agent.onSearchQuery("appointment_time", async (_call, query) => {
  return datetimeFilter.filter(query, { maxResults: 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
export const COMPLETE_PY = `@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.")`;
```

```typescript
export const COMPLETE_TS = `agent.onTaskComplete("schedule_appointment", async (call) => {
  await 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
export const RUN_PY = `# Run agent.call_phone to start the outbound call, setting our initial variables.
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})`;
```

```typescript
export const RUN_TS = `agent.callPhone(process.env.GUAVA_AGENT_NUMBER, toNumber, {
  patientName: patientName,
});

// Or, test your agent in a text-based chat session in the terminal (no audio required).
agent.chat({ patientName: patientName });`;
```

### Complete example

```python
export const FULL_PY = `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},
    )`;
```

```typescript
export const FULL_TS = `import * as guava from "@guava-ai/guava-sdk";
import { DatetimeFilter } from "@guava-ai/guava-sdk/helpers/openai";
import { mockAppointmentsForFuture } from "@guava-ai/guava-sdk/example-data";

const agent = new guava.Agent({
  organization: "Bright Smile Dental",
  purpose: "You are calling patients to help them schedule a dental appointment",
});

const datetimeFilter = new DatetimeFilter({
  sourceList: mockAppointmentsForFuture(),
});

agent.onCallStart(async (call: guava.Call) => {
  await call.reachPerson(await call.getVariable("patientName"));
});

agent.onSearchQuery("appointment_time", async (_call, query) => {
  return datetimeFilter.filter(query, { maxResults: 3 });
});

agent.onReachPerson(async (call: guava.Call, outcome: string) => {
  if (outcome === "available") {
    await call.setTask({
      taskId: "schedule_appointment",
      checklist: [
        "Tell them that it's been a while since their regular cleaning with Dr. Teeth.",
        guava.Field({
          key: "appointment_time",
          fieldType: "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.",
      ],
    });
  } else {
    await call.hangup("Apologize for your mistake and hang up the call.");
  }
});

agent.onTaskComplete("schedule_appointment", async (call) => {
  await call.hangup("Thank them for their time and hang up the call.");
});

export async function run(args: string[]) {
  if (args.includes("--chat")) {
    const patientName = args[args.indexOf("--chat") + 1] ?? "Benjamin Buttons";
    await agent.chat({ patientName });
    return;
  }

const [toNumber, patientName = "Benjamin Buttons"] = args;

if (!toNumber) {
    console.error("Usage: guava-example scheduling-outbound <phone> [name]");
    console.error("       guava-example scheduling-outbound --chat [name]");
    process.exit(1);
  }

agent.callPhone(process.env.GUAVA_AGENT_NUMBER, toNumber, {
    patientName: patientName,
  });
}

if (import.meta.main) {
  run(process.argv.slice(2));
};`;
```
