outbound scheduling.md
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.
export const AGENT_PY = `import guava
agent = guava.Agent(
organization="Bright Smile Dental",
purpose="Call patients to help them schedule a dental appointment.",
)`;
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.
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)`;
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.
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"),
)`;
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.
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.",
],
)`;
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.
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)`;
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.
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.")`;
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().
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})`;
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
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},
)`;
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));
};`;