Skip to content

Quickstart

Welcome to Guava, the newest product from Gridspace that has been designed and created exclusively for developers. Guava allows developers to quickly deliver voice agents that integrate seamlessly into existing agentic AI systems while avoiding prompt bloat.

The primary interface for using Guava is a Python SDK.

Installation / Setup

Pre-requisites: >= Python 3.10

1. Install the Python SDK. There are multiple ways to install - choose your preferred method.

Method 1: pip

$ pip install gridspace-guava --extra-index-url https://guava-pypi.gridspace.com

Method 2: uv astral

$ uv add gridspace-guava --index guava=https://guava-pypi.gridspace.com

Method 3: poetry

$ poetry source add --priority=explicit guava https://guava-pypi.gridspace.com
$ poetry add --source guava gridspace-guava

2. You will need two environment variables to use the SDK.

$ export GUAVA_API_KEY="..."
$ export GUAVA_AGENT_NUMBER="..."

3. You’re ready.

Running an Example

Examples can be found in the gauva.examples submodule. When you run an example it will start a voice conversation through a phone or webrtc session.

Outbound Phone Call

$ python -m guava.examples.property_insurance +1... # Use your phone number here and your agent will call you.

Inbound Phone Call

$ python -m guava.examples.thai_palace
$ # Now dial your agent's number while the script is running.

Inbound (WebRTC)

$ python -m guava.tools.create_webrtc_agent # Create a WebRTC agent code.
$ python -m guava.examples.inbound_webrtc grtc-... # Start the listener
$ # Go to https://guava-dev.gridspace.com/debug-webrtc and call the agent.

Example Walthroughs

Outbound Sales Call With RAG

In this example, using just a few lines of Python, we will call a potential customer for a real estate company and answer their questions by referencing a knowledge base. We will start with a new python file named insurance_example.py.

# insurance_example.py

class InsuranceCallController(guava.CallController):
  def __init__(self):
    super().__init__()

The first step is to define a CallController subclass. The CallController implements a few callback functions that steer the voice experience during the call. You can think of Guava as providing the Agent, and your CallController is the expert coach, whispering in the Agent's ear as they handle the call.

Since we want our bot to provide truthful answers to the customer's questions, the Agent will invoke the on_question function any time the user asks something that can't be inferred from context alone. We'll receive the question in natural language, and we are then free to use any technologies and tools at our disposal to return the answer. The Agent will continue to be responsive and attentive to the conversation while waiting for our response, so we are not latency-constrained in our on_question implementation.

For this example, we wish to store some documents in a vector store and use a RAG system to find the answer.

from guava.examples.example_data import PROPERTY_INSURANCE_POLICY
from guava.helpers.openai import DocumentQA

Luckily, the Guava SDK provides helpers for common CallController implementations, including a DocumentQA class, which is an LLM-based knowledge base implementation. In your own programs, you are free to use any models or providers to handle the Guava callbacks.

class InsuranceCallController(guava.CallController):
  def __init__(self):
    super().__init__()
    self.document_qa = DocumentQA("harper-valley-property-insurance", PROPERTY_INSURANCE_POLICY)

  @override
  def on_question(self, question: str) -> str:
    return self.document_qa.ask(question)

We initialize the document store in our constructor, and then perform a query in the on_question callback. The ask function returns the answer to our question (if the information exists in the document set) in natural language.

Next, we need to give the Agent some information about the job to be done on this call. Unlike some AI platforms, Guava discourages long system prompts that attempt to cover every possible scenario, in favor of short contextual directions. Nevertheless, our Agent will need some guidance at the start of the call to know what to do, so let's use the set_persona and set_task functions in the constructor:

class InsuranceCallController(guava.CallController):
  def __init__(self):
    super().__init__()
    self.document_qa = DocumentQA("harper-valley-property-insurance", PROPERTY_INSURANCE_POLICY)
    self.set_persona(organization_name="Harper Valley Property Insurance")
    self.set_task(
      '''
      You are making an outbound call to a potential customer.
      Your task is to answer questions regarding property insurance
      policy until there are no more questions.
      '''
    )

The set_persona function lets the Agent know what organization it is representing on the call. The set_task function gives the Agent a brief summary of its current objective. As the conversation progresses, we may call set_task again to change our short-term objective, for example to authenticate the user or fill out a form. We'll see other examples of call steering in later examples. Since we are just answering questions for now, this task description plus the on_question handler are enough for a complete voice experience.

With all that in place, it's time to start talking!

if __name__ == "__main__":
  guava.Client().create_outbound(
    from_number=os.environ['GUAVA_AGENT_NUMBER'],
    to_number="+1...", # Your phone number goes here.
    call_controller=InsuranceCallController(),
  )

The guava.Client() constructor will open a new control websocket to the Guava API server, and create_outbound will initiate an outbound phone call. To test our Agent, we just run the script, making sure that our API key and agent number variables are set:

$ export GUAVA_API_KEY="..."
$ export GUAVA_AGENT_NUMBER="..."
$ python ./insurance_example.py

The full insurance_example.py file is shown below.

# insurance_example.py

import guava
import os
from typing_extensions import override
from guava.examples.example_data import PROPERTY_INSURANCE_POLICY
from guava.helpers.openai import DocumentQA

class InsuranceCallController(guava.CallController):
  def __init__(self):
    super().__init__()
    # Load in a QA system based off a long and complex property insurance document.
    self.document_qa = DocumentQA("harper-valley-property-insurance", PROPERTY_INSURANCE_POLICY)
    self.set_persona(organization_name="Harper Valley Property Insurance")
    self.set_task("You are making an outbound call to a potential customer. Your task is to answer questions regarding property insurance policy until there are no more questions.")

  @override
  def on_question(self, question: str) -> str:
    # Replace with your fancy RAG system.
    return self.document_qa.ask(question)

if __name__ == "__main__":
  guava.Client().create_outbound(
    from_number=os.environ['GUAVA_AGENT_NUMBER'],
    to_number="+1...", # Your phone number goes here.
    call_controller=InsuranceCallController(),
  )

Inbound Reservation Call With Intent Recognition and Field Collection

In this example, we will walk through how to set up an inbound call listener for a restaurant. When a caller phones in to make a reservation, the Agent listens for the caller’s intent, routes it to your CallController, and responds appropriately. By the end of this walkthrough, you’ll see how an inbound call flows from the initial greeting all the way to capturing a reservation request.

We will start with a new Python file named restaurant_example.py.

# restaurant_example.py

class RestaurantReservationCallController(guava.CallController):
  def __init__(self):
    super().__init__()

For our first step, to customize our inbound call voice experience, we will define a CallController subclass called RestaurantReservationCallController.

When the Agent detects an action or intent expressed by the caller, it needs a way to notify our RestaurantReservationCallController appropriately based on what the caller is trying to do. To support this, the Agent invokes the on_intent method whenever a caller intent is detected. The detected intent is passed to on_intent in natural language, giving you the flexibility to use any tools, services, or logic needed to determine the next steps. While this processing occurs, the Agent remains engaged in the conversation with the caller, ensuring the caller experience stays responsive and is not blocked by downstream logic.

How can we set things up such that our RestaurantReservationCallController is able to process downstream logic on a select set of intents that the Agent detects?

from guava.helpers.openai import IntentRecognizer

The Guava SDK provides a helper IntentRecognizer class, an LLM-based classifier. The SDK reference for IntentRecognizer can be found here.

class RestaurantReservationCallController(guava.CallController):
  def __init__(self):
    super().__init__()
    self.intent_recognizer = IntentRecognizer(["making a reservation", "anything else"])

  @override
  def on_intent(self, intent: str):
    choice = self.intent_recognizer.classify(intent)
    if choice == "making a reservation":
      ...
    else:
      ...

We initialize the intent recognizer in the constructor by providing a list of intents we want to classify. When an intent is detected and matched, we handle it in the on_intent callback, where we can take action based on the matched intent. The classify method returns the most likely intent from the list provided during initialization.

With intent classification in place, we will use the set_persona and set_task methods to give the Agent its own background and guide its behavior, both at the start of the call and whenever a caller’s intent is detected.

class RestaurantReservationCallController(guava.CallController):
  def __init__(self):
    super().__init__()
    self.intent_recognizer = IntentRecognizer(["making a reservation", "anything else"])
    self.set_persona(organization_name="Thai Palace")
    self.make_reservation()

  def make_reservation(self):
    self.set_task(
      objective="You are a virtual assistant for a restaurant called Thai Palace. Your job is to add callers to a reservation list.",
      checklist=[
        guava.Field(
          key="caller_name",
          field_type="text",
          description="The name to be added to the waitlist"
        ),
        guava.Field(
          key="party_size",
          field_type="integer",
          description="The number of people attending",
        ),
        guava.Field(
          key="phone_number",
          field_type="text",
          description="Phone number to text when the table is ready",
        ),
        "Read the phone number back to the caller to make sure you got it right.",
      ],
      on_complete=self.hangup,
    )

  @override
  def on_intent(self, intent: str):
    choice = self.intent_recognizer.classify(intent)
    if choice == "making a reservation":
      self.make_reservation()
    else:
      self.set_task(
        checklist=["Tell the caller that we only handle restaurant reservation at this number."],
        on_complete=self.make_reservation,
      )

The set_persona method establishes that the agent is representing the Thai Palace restaurant for these calls.

Beyond defining the Agent’s current objective through the objective argument, the set_task method also allows you to supply a checklist (a structured list of to-do items the agent must complete) and an on_complete callback (invoked once all checklist items have been satisfied).

In this reservation example, we need to collect a few pieces of information from the caller: their name, the number of guests, and a phone number for the reservation. To gather this information, we populate the checklist with guava.Field(...) entries, one for each required detail. We can also include natural language in the form of a plain Python string in the checklist to provide the Agent with more flexible, high-level guidance on how to conduct the conversation.

Once all checklist items have been completed, we pass the hangup method (provided by the Guava SDK as part of the CallController interface) as the on_complete callback, allowing the Agent to gracefully end the call when the reservation making process is finished.

For more details on set_task and the supported checklist item types, refer to the SDK reference here.

class RestaurantReservationCallController(guava.CallController):
  def __init__(self):
    super().__init__()
    self.intent_recognizer = IntentRecognizer(["making a reservation", "anything else"])
    self.set_persona(organization_name="Thai Palace")
    self.make_reservation()
    self.accept_call()

Unlike the outbound example, inbound calls require an explicit decision about how to handle incoming callers. While the Client waits and listens for inbound calls, our RestaurantReservationCallController is responsible for deciding whether a call should be accepted or rejected. This is done using the accept_call() method, which allows the Agent to answer and begin handling the call. In some cases, you may instead choose to call reject_call() (for example, if the incoming number is on a blacklist or does not meet your acceptance criteria).

With all that in place, let's set up our Client to start listening for inbound calls.

if __name__ == "__main__":
  guava.Client().listen_inbound(
    agent_number=os.environ["GUAVA_AGENT_NUMBER"],
    controller_class=RestaurantReservationCallController,
  )

The guava.Client() constructor will open a new control websocket to the Guava API server, and listen_inbound will start listening for inbound calls. To test our Agent, we just run the script, making sure that our API key and agent number variables are set:

$ export GUAVA_API_KEY="..."
$ export GUAVA_AGENT_NUMBER="..."
$ python ./restaurant_example.py

The full restaurant_example.py file is shown below.

# restaurant_example.py

import guava
import os
from typing_extensions import override
from guava.helpers.openai import IntentRecognizer

class RestaurantReservationCallController(guava.CallController):
  def __init__(self):
    super().__init__()
    self.intent_recognizer = IntentRecognizer(["making a reservation", "anything else"])
    self.set_persona(organization_name="Thai Palace")
    self.make_reservation()
    self.accept_call()


  def make_reservation(self):
    self.set_task(
      objective="You are a virtual assistant for a restaurant called Thai Palace. Your job is to add callers to a reservation list.",
      checklist=[
        guava.Field(
          key="caller_name",
          field_type="text",
          description="The name to be added to the waitlist"
        ),
        guava.Field(
          key="party_size",
          field_type="integer",
          description="The number of people attending",
        ),
        guava.Field(
          key="phone_number",
          field_type="text",
          description="Phone number to text when the table is ready",
        ),
        "Read the phone number back to the caller to make sure you got it right.",
      ],
      on_complete=self.hangup,
    )

  @override
  def on_intent(self, intent: str):
    choice = self.intent_recognizer.classify(intent)
    if choice == "making a reservation":
      self.make_reservation()
    else:
      self.set_task(
        checklist=["Tell the caller that we only handle restaurant reservation at this number."],
        on_complete=self.make_reservation,
      )


if __name__ == "__main__":
  guava.Client().listen_inbound(
    agent_number=os.environ["GUAVA_AGENT_NUMBER"],
    controller_class=RestaurantReservationCallController,
  )