everything.md

import { NextLink, Callout } from '../views/docs/prose'; import { ExpertAnimation } from '../views/docs/ExpertAnimation'; import { CodeTabs } from '../views/docs/CodeTabs';

Architecture Overview

Every Guava call involves two systems working in parallel: Guava's hosted Dialog System and your Expert — a service you provide that connects to Guava's API and steers the conversation.

GUAVA CLOUD Dialog System audio · STT · LLM · TTS Caller audio WebSocket Your Expert Python · TypeScript · ... Your Infrastructure local or self-hosted

or

Guava Hosting managed by Guava

The Dialog System

The Dialog System is Guava's managed service running in the cloud. It handles everything time-sensitive during a call: receiving the caller's audio, transcribing it, synthesizing a response, and streaming it back to the caller.

Because the entire pipeline runs as a fully integrated architecture rather than a chain of off-the-shelf APIs, the Dialog System delivers best-in-class latency and naturalness.

The Expert

The Expert is the code you write. Using the Guava SDK, it connects to the Dialog System over a persistent WebSocket and steers the agent in real time.

Because your Expert is just code, you can do anything: query a CRM or database, hit an external API, or chain into another specialized AI sub-agent. The Dialog System always interacts with your Expert asynchronously, so you can spend time on complex tasks and reasoning without the caller ever noticing a pause.

During development, your Expert runs on your local machine, and Guava routes calls to it directly. You can rapidly iterate by changing the code and restarting the process — no public web server or ngrok required.

Structured Callbacks

As the Dialog System converses with the caller in natural language, it maintains a separate communication channel with your Expert. This channel consists of callbacks that follow a consistent structure and schema, and are designed to be plugged into backend systems like RAG and intent recognition. You decide which callbacks your Expert can handle — all are optional, and the Dialog System will adapt appropriately.

export const ARCH_CALLBACKS_PY = `@agent.on_question def on_question(call: guava.Call, question: str) -> str: # The agent will invoke this when the caller asks a question that can't be # answered from the current context. Use our ready-to-use RAG module. Or # plug in your own custom one. return document_qa.ask(question)

@agent.on_action_request def on_action_request(call: guava.Call, request: str) -> list[SuggestedAction]: # The agent will invoke this callback when the caller requests a new action. # Use our ready-to-use intent classification system. Or plug in a custom one. return intent_recognizer.classify(request)`;

export const ARCH_CALLBACKS_TS = `agent.onQuestion(async (call: guava.Call, question: string) => { // The agent will invoke this when the caller asks a question that can't be // answered from the current context. Use our ready-to-use RAG module. Or // plug in your own custom one. return await documentQA.ask(question); });

agent.onActionRequest(async (call: guava.Call, request: string) => { // The agent will invoke this callback when the caller requests a new action. // Use our ready-to-use intent classification system. Or plug in a custom one. return await intentRecognizer.classify(request); });`;

<CodeTabs python={{ code: ARCH_CALLBACKS_PY, filename: "expert.py" }} typescript={{ code: ARCH_CALLBACKS_TS, filename: "expert.ts" }} />

If you want ready-to-use implementations of these callbacks, you can use our helper library which contains implementations of RAG, intent recognition, and more. But all of these are optional modules and we encourage you to build your own domain specific versions.

Assign Tasks to your Agents

Instead of using a single large prompt for your agent, Guava recommends that you use Tasks. A task is a checklist of items that you assign to your agent. These items can include things to say to the caller, as well as information to collect as Fields. You will receive a callback when your Agent has completed your task and is awaiting more instructions.

export const ARCH_TASK_PY = `@agent.on_call_start def on_call_start(call: guava.Call): call.set_task( "waitlist", objective="Add callers to the waitlist.", checklist=[ guava.Field(key="caller_name", field_type="text"), guava.Field(key="party_size", field_type="integer"), guava.Field(key="phone_number", field_type="text"), "Read the phone number back to the caller to confirm.", ], )

@agent.on_task_complete("waitlist") def on_waitlist_done(call: guava.Call): print("Added caller to waitlist:", call.get_field("caller_name")) # End the call, transfer, or chain into another task. call.hangup("Thank the caller and let them know we'll text when their table is ready.")`;

export const ARCH_TASK_TS = `agent.onCallStart(async (call) => { await call.setTask({ taskId: "waitlist", objective: "Add callers to the waitlist.", checklist: [ guava.Field({ key: "caller_name", fieldType: "text" }), guava.Field({ key: "party_size", fieldType: "integer" }), guava.Field({ key: "phone_number", fieldType: "text" }), "Read the phone number back to the caller to confirm.", ], }); });

agent.onTaskComplete("waitlist", async (call) => { console.log("Added caller to waitlist:", await call.getField("caller_name")); // End the call, transfer, or chain into another task. await call.hangup("Thank the caller and let them know we'll text when their table is ready."); });`;

<CodeTabs python={{ code: ARCH_TASK_PY, filename: "expert.py" }} typescript={{ code: ARCH_TASK_TS, filename: "expert.ts" }} />

Deploying your Expert

When it's time to move to production, you'll want your Expert deployed in a highly-available configuration, ready to handle calls at any time. Because Guava Experts only make outbound connections, it's easy to run an Expert behind a NAT or firewall.

You have two options for deploying your Expert:

  • Your Infrastructure — deploy to your own servers, VM, or serverless compute platform. You control the environment.
  • Guava Hosting — push your Expert with a single guava deploy command and Guava manages the rest, including horizontal scaling and redundancy.

See the Deployment guide for a full walkthrough of both options.

What to read next

  • The Quickstart shows you how to set up your dev environment and create your first agent.
  • The Example Walkthroughs show full Expert implementations for common scenarios, including Q&A and scheduling.
  • Once you're comfortable with the basics, the SDK Reference covers every callback and command in detail.

import { CodeBlock } from '../views/docs/CodeBlock'; import { NextLink, Callout, Prose } from '../views/docs/prose'; import { PlatformTabs } from '../views/docs/PlatformTabs'; import { ManualInstallDownloads } from '../views/docs/ManualInstallDownloads';

Quickstart

The recommended way to build Guava voice agents is using the Guava CLI, which bootstraps projects and installs the SDK. If you’d rather not install the CLI, you can skip to the [direct SDK installation guide](./sdk-installation) instead.

Install the CLI

Install the CLI using one of the supported methods for your platform.

<PlatformTabs macosContent={<> If you have Homebrew installed, you can install the CLI from our tap. <CodeBlock code={brew install goguava-ai/tap/guava} language="bash" /> If not, you can install using our provided shell script. <CodeBlock code={# Installs to \/.local/bin/guava` curl -fsSL https://goguava.ai/install.sh | sh} language="bash" /> </>} linuxContent={<> <Prose>For Linux and WSL, run the installation shell script.</Prose> <CodeBlock code={# Installs to `/.local/bin/guava` curl -fsSL https://goguava.ai/install.sh | sh`} language="bash" /> </>} windowsContent={<> Run the installation script from a PowerShell console. </>} manualContent={<> Download the binary for your platform directly. After downloading, make the file executable and place it somewhere on your PATH (e.g. ~/.local/bin/). </>} />

Authenticate the CLI

This command will open a browser where you can log in or create an account.

Create an Agent

Scaffold a new agent project with starter code (currently Python only).

Test your Agent

You can stop your agent by pressing Ctrl-C.

Deploy your Agent

Track status in Deployments. Every call appears in Conversations.

Stop your agent after deploying

Be sure to stop your agent after deploying.


import { CodeBlock } from '../views/docs/CodeBlock'; import { LanguageTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink, Prose } from '../views/docs/prose';

Quickstart (SDK-Only)

The recommended way to build Guava voice agents is using the [Guava CLI](./quickstart), which bootstraps projects and installs the SDK. This guide explains how to install the SDK directly if you’d prefer not to use the CLI.

Create an account

Sign up for an account at app.goguava.ai.

Install the SDK

Guava provides SDKs for Python and TypeScript. Choose your language and package manager.

<LanguageTabs pythonContent={<> <CodeBlock code={ uv add guava-sdk # Install using uv (Recommended) pip install guava-sdk # Install using pip poetry add guava-sdk # Install using poetry} language="bash" /> </>} typescriptContent={<> <CodeBlock code={ npm install @guava-ai/guava-sdk # Install using npm yarn add @guava-ai/guava-sdk # Install using yarn pnpm add @guava-ai/guava-sdk # Install using pnpm} language="bash" /> </>} />

Set Environment Variables

The SDK reads credentials from the environment automatically. Set these before running any Guava scripts.

<CodeBlock code={export GUAVA_API_KEY="gva-..." # Set to your API key. export GUAVA_AGENT_NUMBER="+15551234567" # Used by SDK examples. Set to your purchased number.} filename=".env" language="bash" />

Create an API key using the API Keys page. Purchase a phone number using the Phone Numbers page.

Add the coding agent starter kit

Download the Guava coding agent starter kit into your project. It contains plain-text API docs sized for AI coding assistants.

<CodeBlock code={curl -o guava-docs.md https://goguava.ai/docs/coding-agent-starter.md} language="bash" />

Run an Example

<LanguageTabs pythonContent={<> Examples can be run directly from the Python SDK. You can browse the examples on GitHub. <CodeBlock code={
`# This will attach your agent to a phone number. Dial that number to talk to your agent. python -m guava.examples.restaurant_waitlist --phone

Start a test call with a local audio device.

python -m guava.examples.restaurant_waitlist --local

This will attach your agent to a WebRTC code.

You can dial it from the browser at https://app.goguava.ai/debug-webrtc

python -m guava.examples.restaurant_waitlist --webrtc

Start an in-terminal text-only chat for testing.

python -m guava.examples.restaurant_waitlist --chat} language="bash" /> </>} typescriptContent={<> <Prose>Examples can be run directly from the TypeScript SDK. You can browse the examples <a href="https://github.com/goguava-ai/typescript-sdk/tree/main/examples">on GitHub</a></Prose> <CodeBlock code={ # This will attach your agent to a phone number. Dial that number to talk to your agent. npx @guava-ai/guava-sdk restaurant-waitlist --phone

Start a test call with a local audio device.

npx @guava-ai/guava-sdk restaurant-waitlist --local

This will attach your agent to a WebRTC code.

You can dial it from the browser at https://app.goguava.ai/debug-webrtc

npx @guava-ai/guava-sdk restaurant-waitlist --webrtc

Start an in-terminal text-only chat for testing.

npx @guava-ai/guava-sdk restaurant-waitlist --chat`} language="bash" /> </>} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, Prose, NextLink } from '../views/docs/prose';

Inbound Example w/ RAG

In this example, we'll build an inbound voice agent for a fictional property insurance company. Callers can ask any question about their policy and receive accurate answers sourced from a policy document.

Define the Agent

guava.Agent is our starting point for building Guava agents. We'll start by creating one with some basic background details.

export const AGENT_PY = `import guava

agent = guava.Agent( organization="Harper Valley Property Insurance", purpose="Answer questions regarding property insurance policy until there are no more questions", )`;

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

const agent = new guava.Agent({ organization: "Harper Valley Property Insurance", purpose: "Answer questions regarding property insurance policy until there are no more questions", });`;

<CodeTabs python={{ code: AGENT_PY, filename: "property_insurance.py" }} typescript={{ code: AGENT_TS, filename: "property-insurance.ts" }} />

Guava discourages long system prompts that try to cover every scenario. The `purpose` is intentionally short and designed to orient the agent.

Set up DocumentQA

Next, we initialize a DocumentQA instance with the policy document. DocumentQA is a built-in RAG that covers a lot of simple use cases. It's a fully pluggable component and we expect many users will bring their own RAG system.

export const QA_PY = `from guava.helpers.rag import DocumentQA from guava.examples.example_data import PROPERTY_INSURANCE_POLICY

document_qa = DocumentQA(documents=PROPERTY_INSURANCE_POLICY)`;

export const QA_TS = `import { DocumentQA } from "@guava-ai/guava-sdk/helpers/openai"; import { PROPERTY_INSURANCE_POLICY } from "@guava-ai/guava-sdk/example-data";

const documentQA = new DocumentQA("harper-valley-property-insurance", PROPERTY_INSURANCE_POLICY);`;

<CodeTabs python={{ code: QA_PY, filename: "property_insurance.py" }} typescript={{ code: QA_TS, filename: "property-insurance.ts" }} />

Handle questions with on_question

Whenever the caller asks something the agent cannot answer from context alone, Guava invokes the on_question callback with the question in natural language. We forward it to DocumentQA and return the answer.

export const ON_QUESTION_PY = @agent.on_question def on_question(call: guava.Call, question: str) -> str: return document_qa.ask(question);

export const ON_QUESTION_TS = agent.onQuestion(async (call: guava.Call, question: string) => { return await documentQA.ask(question); });;

<CodeTabs python={{ code: ON_QUESTION_PY, filename: "property_insurance.py" }} typescript={{ code: ON_QUESTION_TS, filename: "property-insurance.ts" }} />

The agent remains fully responsive during the lookup — it continues listening and engaging with the caller while waiting for your response. You are not latency-constrained in your on_question implementation.

Bring your own RAG. The on_question callback receives a plain string and expects a plain string back — you can plug in any knowledge base, vector store, or model you prefer.

Start the agent

Finally, we attach the agent to a channel so that we can actually talk to it.

export const RUN_PY = `# Run this to attach your agent to a phone number. Call your agent's number to talk to it. agent.listen_phone(os.environ["GUAVA_AGENT_NUMBER"])

Run this to receive a WebRTC link where you can talk to your agent in the browser.

agent.listen_webrtc()

Run this to talk to your agent using your local audio device.

agent.call_local()

Run this to test your agent in a text-based chat session in the terminal (no audio required).

agent.chat()`;

export const RUN_TS = `// Run this to attach your agent to a phone number. Call your agent's number to talk to it. agent.listenPhone(process.env.GUAVA_AGENT_NUMBER!);

// Run this to receive a WebRTC link where you can talk to your agent in the browser. agent.listenWebrtc();

// Run this to talk to your agent using your local audio device. agent.callLocal();

// Run this to test your agent in a text-based chat session in the terminal (no audio required). agent.chat();`;

<CodeTabs python={{ code: RUN_PY, filename: "property_insurance.py" }} typescript={{ code: RUN_TS, filename: "property-insurance.ts" }} />

No web servers required. Guava does not require a public web server to receive inbound calls. All Guava agents can be hosted behind firewalls and NATs.

Complete example

export const FULL_PY = `import logging import os import guava import argparse

from guava.helpers.rag import DocumentQA from guava import logging_utils, Agent from guava.examples.example_data import PROPERTY_INSURANCE_POLICY

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

agent = Agent( organization="Harper Valley Property Insurance", purpose="Answer questions regarding property insurance policy until there are no more questions", )

This is a built-in knowledge base helper that we will use for this example.

You can use any RAG system you prefer.

document_qa = DocumentQA(documents=PROPERTY_INSURANCE_POLICY)

When the Agent is asked a question that it cannot answer, it will invoke the on_question callback.

@agent.on_question def on_question(call: guava.Call, question: str) -> str: # Forward the Agent's question to the knowledge base and return the answer. # You can plug in any knowledge base system you want here. answer = document_qa.ask(question) logger.info("RAG answer: %s", answer) return answer

if name == "main": logging_utils.configure_logging()

Every Agent can be attached to multiple resources.

parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--phone", action="store_true", help="Listen for phone calls.")
group.add_argument("--webrtc", action="store_true", help="Create on a WebRTC code.")
group.add_argument("--local", action="store_true", help="Start a local call.")
group.add_argument("--chat", action="store_true", help="Start a text-based chat session for testing.")
args = parser.parse_args()

if args.phone: agent.listen_phone(os.environ["GUAVA_AGENT_NUMBER"]) elif args.webrtc: agent.listen_webrtc() elif args.chat: agent.chat() else: agent.call_local()`;

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

const agent = new guava.Agent({ organization: "Harper Valley Property Insurance", purpose: "Answer questions regarding property insurance policy until there are no more questions", });

// This is a built-in knowledge base helper that we will use for this example. // You can use any RAG system you prefer. const documentQA = new DocumentQA("harper-valley-property-insurance", PROPERTY_INSURANCE_POLICY);

// When the Agent is asked a question that it cannot answer, it will invoke the on_question callback. agent.onQuestion(async (call: guava.Call, question: string) => { // Forward the Agent's question to the knowledge base and return the answer. // You can plug in any knowledge base system you want here. return await documentQA.ask(question); });

const args = process.argv.slice(2); if (args.includes("--webrtc")) { agent.listenWebrtc(); } else if (args.includes("--phone")) { agent.listenPhone(process.env.GUAVA_AGENT_NUMBER!); } else if (args.includes("--local")) { agent.callLocal(); } else if (args.includes("--chat")) { agent.chat(); } else { console.error("Usage: guava-example property-insurance --phone | --webrtc | --local | --chat"); process.exit(1); }`;

<CodeTabs python={{ code: FULL_PY, filename: "property_insurance.py" }} typescript={{ code: FULL_TS, filename: "property-insurance.ts" }} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, Prose, NextLink } from '../views/docs/prose';

Inbound w/ Form Filling

In this example, we'll build an inbound voice agent for a fictional restaurant. Callers can join the waitlist by providing their name, party size, and a callback number — which the agent collects conversationally using a structured task.

Define the Agent

guava.Agent is our starting point for building Guava agents. We'll create one with a name and purpose scoped to the restaurant.

export const AGENT_PY = `import guava

agent = guava.Agent( name="Mia", organization="Thai Palace", purpose="Helping callers join the restaurant waitlist", )`;

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

const agent = new guava.Agent({ name: "Mia", organization: "Thai Palace", purpose: "Helping callers join the restaurant waitlist", });`;

<CodeTabs python={{ code: AGENT_PY, filename: "restaurant_waitlist.py" }} typescript={{ code: AGENT_TS, filename: "restaurant-waitlist.ts" }} />

Accept or reject the call

on_call_received fires before the call starts and gives you a chance to accept or reject based on caller info. Here we accept all calls.

export const ACCEPT_PY = @agent.on_call_received def on_call_received(call_info: guava.CallInfo) -> guava.IncomingCallAction: return guava.AcceptCall();

export const ACCEPT_TS = agent.onCallReceived(async (_callInfo: guava.CallInfo) => { return { action: "accept" }; });;

<CodeTabs python={{ code: ACCEPT_PY, filename: "restaurant_waitlist.py" }} typescript={{ code: ACCEPT_TS, filename: "restaurant-waitlist.ts" }} />

If you don't register on_call_received, Guava accepts all calls by default. Implement it only when you need to screen callers or look up information based off the incoming phone number.

Set up the form

on_call_start fires at the beginning of every accepted call. We use set_task to hand the agent a structured checklist of fields to collect. The agent gathers each piece of information conversationally — it knows when all fields are filled and automatically moves on.

export const TASK_PY = @agent.on_call_start def on_call_start(call: guava.Call) -> None: call.set_task( "waitlist", objective="You are a virtual assistant for Thai Palace. Add callers to the waitlist.", checklist=[ guava.Field(key="caller_name", field_type="text", description="Name for the waitlist"), guava.Field(key="party_size", field_type="integer", description="Number of people"), 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 confirm.", ], );

export const TASK_TS = agent.onCallStart(async (call: guava.Call) => { await call.setTask({ taskId: "waitlist", objective: "You are a virtual assistant for Thai Palace. Add callers to the waitlist.", checklist: [ guava.Field({ key: "caller_name", fieldType: "text", description: "Name for the waitlist" }), guava.Field({ key: "party_size", fieldType: "integer", description: "Number of people" }), guava.Field({ key: "phone_number", fieldType: "text", description: "Phone number to text when the table is ready", }), "Read the phone number back to the caller to confirm.", ], }); });;

<CodeTabs python={{ code: TASK_PY, filename: "restaurant_waitlist.py" }} typescript={{ code: TASK_TS, filename: "restaurant-waitlist.ts" }} />

The checklist can mix Field objects (typed, named values the agent extracts) with plain strings (freeform instructions the agent follows). Fields are retrievable later via get_field().

Handle task completion

on_task_complete fires once every field in the checklist is collected. This is the right place to save the data to your backend, trigger a notification, or hang up.

export const COMPLETE_PY = @agent.on_task_complete("waitlist") def on_waitlist_done(call: guava.Call) -> None: logger.info( "Added %s, party of %d, to waitlist.", call.get_field("caller_name"), call.get_field("party_size"), ) call.hangup("Thank the caller and let them know we'll text when their table is ready.");

export const COMPLETE_TS = agent.onTaskComplete("waitlist", async (call: guava.Call) => { logger.info( "Added %s, party of %d, to waitlist.", await call.getField("caller_name"), await call.getField("party_size"), ); await call.hangup("Thank the caller and let them know we'll text when their table is ready."); });;

<CodeTabs python={{ code: COMPLETE_PY, filename: "restaurant_waitlist.py" }} typescript={{ code: COMPLETE_TS, filename: "restaurant-waitlist.ts" }} />

Start the agent

Attach the agent to a channel to start receiving inbound calls.

Run this to receive a WebRTC link where you can talk to your agent in the browser.

agent.listen_webrtc()

Run this to talk to your agent using your local audio device.

agent.call_local()

Run this to test your agent in a text-based chat session in the terminal (no audio required).

agent.chat()`;

// Run this to receive a WebRTC link where you can talk to your agent in the browser. agent.listenWebrtc();

// Run this to talk to your agent using your local audio device. agent.callLocal();

// Run this to test your agent in a text-based chat session in the terminal (no audio required). agent.chat();`;

<CodeTabs python={{ code: RUN_PY, filename: "restaurant_waitlist.py" }} typescript={{ code: RUN_TS, filename: "restaurant-waitlist.ts" }} />

Complete example

export const FULL_PY = `import os import guava import logging import argparse from guava import logging_utils

logger = logging.getLogger("thai_palace")

agent = guava.Agent( name="Mia", organization="Thai Palace", purpose="Helping callers join the restaurant waitlist", )

@agent.on_call_received def on_call_received(call_info: guava.CallInfo) -> guava.IncomingCallAction: return guava.AcceptCall()

@agent.on_call_start def on_call_start(call: guava.Call) -> None: call.set_task( "waitlist", objective="You are a virtual assistant for Thai Palace. Add callers to the waitlist.", checklist=[ guava.Field(key="caller_name", field_type="text", description="Name for the waitlist"), guava.Field(key="party_size", field_type="integer", description="Number of people"), 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 confirm.", ], )

@agent.on_task_complete("waitlist") def on_waitlist_done(call: guava.Call) -> None: logger.info( "Added %s, party of %d, to waitlist.", call.get_field("caller_name"), call.get_field("party_size"), ) call.hangup("Thank the caller and let them know we'll text when their table is ready.")

if name == "main": logging_utils.configure_logging()

parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group(required=True) group.add_argument("--phone", action="store_true", help="Listen for phone calls.") group.add_argument("--webrtc", action="store_true", help="Create on a WebRTC code.") group.add_argument("--local", action="store_true", help="Start a local call.") group.add_argument("--chat", action="store_true", help="Start a text-based chat session for testing.") args = parser.parse_args()

export const FULL_TS = `import * as guava from "@guava-ai/guava-sdk"; import { getDefaultLogger } from "@guava-ai/guava-sdk";

const logger = getDefaultLogger();

const agent = new guava.Agent({ name: "Mia", organization: "Thai Palace", purpose: "Helping callers join the restaurant waitlist", });

agent.onCallReceived(async (_callInfo: guava.CallInfo) => { return { action: "accept" }; });

agent.onCallStart(async (call: guava.Call) => { await call.setTask({ taskId: "waitlist", objective: "You are a virtual assistant for Thai Palace. Add callers to the waitlist.", checklist: [ guava.Field({ key: "caller_name", fieldType: "text", description: "Name for the waitlist" }), guava.Field({ key: "party_size", fieldType: "integer", description: "Number of people" }), guava.Field({ key: "phone_number", fieldType: "text", description: "Phone number to text when the table is ready", }), "Read the phone number back to the caller to confirm.", ], }); });

agent.onTaskComplete("waitlist", async (call: guava.Call) => { logger.info( "Added %s, party of %d, to waitlist.", await call.getField("caller_name"), await call.getField("party_size"), ); await call.hangup("Thank the caller and let them know we'll text when their table is ready."); });

const args = process.argv.slice(2); if (args.includes("--webrtc")) { agent.listenWebrtc(); } else if (args.includes("--phone")) { agent.listenPhone(process.env.GUAVA_AGENT_NUMBER!); } else if (args.includes("--local")) { agent.callLocal(); } else if (args.includes("--chat")) { agent.chat(); } else { console.error("Usage: guava-example restaurant-waitlist --phone | --webrtc | --local | --chat"); process.exit(1); }`;

<CodeTabs python={{ code: FULL_PY, filename: "restaurant_waitlist.py" }} typescript={{ code: FULL_TS, filename: "restaurant-waitlist.ts" }} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, Prose, NextLink } from '../views/docs/prose';

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", });`;

<CodeTabs python={{ code: AGENT_PY, filename: "scheduling_outbound.py" }} typescript={{ code: AGENT_TS, filename: "scheduling-outbound.ts" }} />

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(), });`;

<CodeTabs python={{ code: FILTER_PY, filename: "scheduling_outbound.py" }} typescript={{ code: FILTER_TS, filename: "scheduling-outbound.ts" }} />

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")); });;

<CodeTabs python={{ code: START_PY, filename: "scheduling_outbound.py" }} typescript={{ code: START_TS, filename: "scheduling-outbound.ts" }} />

Under the hood `reach_person()` is just a call to `set_task()`. You can replace `reach_person()` with your own [Task](./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.

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."); } });;

<CodeTabs python={{ code: REACH_PY, filename: "scheduling_outbound.py" }} typescript={{ code: REACH_TS, filename: "scheduling-outbound.ts" }} />

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 }); });;

<CodeTabs python={{ code: SEARCH_PY, filename: "scheduling_outbound.py" }} typescript={{ code: SEARCH_TS, filename: "scheduling-outbound.ts" }} />

The agent may call this handler multiple times if the patient rejects the initial options or refines their availability.

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."); });;

<CodeTabs python={{ code: COMPLETE_PY, filename: "scheduling_outbound.py" }} typescript={{ code: COMPLETE_TS, filename: "scheduling-outbound.ts" }} />

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 });`;

<CodeTabs python={{ code: RUN_PY, filename: "scheduling_outbound.py" }} typescript={{ code: RUN_TS, filename: "scheduling-outbound.ts" }} />

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

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 [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)); }`;

<CodeTabs python={{ code: FULL_PY, filename: "scheduling_outbound.py" }} typescript={{ code: FULL_TS, filename: "scheduling-outbound.ts" }} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink } from '../views/docs/prose';

export const AGENT_EX_PY = `import os import guava

Define our agent

agent = guava.Agent( name="Nova", organization="Acme Corp", purpose="Help customers with their orders.", )

Register handlers

@agent.on_call_start def on_call_start(call: guava.Call): ...

Attach to a channel

agent.listen_phone(os.environ["GUAVA_AGENT_NUMBER"])`;

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

// Define our agent const agent = new guava.Agent({ name: "Nova", organization: "Acme Corp", purpose: "Help customers with their orders.", });

// Register handlers agent.onCallStart(async (call) => { ... });

// Attach to a channel agent.listenPhone(process.env.GUAVA_AGENT_NUMBER!);`;

Agent

guava.Agent is the entrypoint for creating Guava voice agents. Create an Agent instance, attach handlers, then attach to a channel.

<CodeTabs python={{ code: AGENT_EX_PY, filename: "agent.py" }} typescript={{ code: AGENT_EX_TS, filename: "agent.ts" }} />

export const AGENT_SIG_PY = `guava.Agent( # The name the agent uses to identify itself to callers. name: str | None = None,

The organization the agent represents.

organization: str | None = None,

High-level description of the agent's role.

purpose: str | None = None,

)`;

export const AGENT_SIG_TS = `new guava.Agent({ // The name the agent uses to identify itself to callers. name?: string,

// The organization the agent represents. organization?: string,

// High-level description of the agent's role. purpose?: string, })`;

Constructor

Use the constructor parameters to configure your Agent's persona and goal.

<CodeTabs python={{ code: AGENT_SIG_PY }} typescript={{ code: AGENT_SIG_TS }} />

Handlers

Register handlers to control and react to the call in real-time.

Handler Description
on_call_received This handler is invoked on incoming calls. You can chose whether to reject or accept the call. The default behavior if not provided is to accept every call.
on_call_start Called when a call begins. Unlike on_call_received, this handler is invoked for both incoming and outgoing calls. Use this handler to set initial tasks and context for the Agent.
on_caller_speech Called each time the caller speaks.
on_agent_speech Called each time the agent speaks.
on_question Called when the caller asks the agent a question it cannot answer from context alone. The provided answer is relayed back to the caller.
on_task_complete Called when the Agent completes a Task previously set using call.set_task.
on_search_query Provide dynamic search results for a searchable Field.
on_action_request / on_action Called when the caller asks for a specific action, e.g. "can I reset my password?"
on_session_end Called when the session ends. Read event.termination_reason to find out why the call ended.
on_reach_person Called when a reach_person task completes.
on_outbound_failed Called when an outbound call fails to dial.
on_escalate Called when an escalation is triggered.

Entrypoints / Channels

Attach the agent to a channel to start receiving calls.

Entrypoint Description
listen_phone("+1...") Listen for inbound phone calls on the given phone number.
listen_webrtc("grtc-..." | None) Listen for inbound WebRTC connections to the given agent code. If not provided, a temporary agent code is automatically created.
listen_sip("guavasip-...") Listen for inbound SIP connections to the given SIP code.
call_phone(from_number, to_number, variables?) Place a single outbound phone call.
call_local() Call the agent using your local audio device (for testing).
attach_campaign(campaign) Attach an agent to an outbound Campaign.
chat(variables?) Start an interactive terminal chat session with the agent (for testing).
test(variables?) Starts a live test session for programmatic testing.
test_roleplay(roleplay_prompt, variables?) Run an automated test where an LLM roleplays as the caller.
To attach an agent to multiple channels, or run multiple agents in the same process, use guava.Runner.

import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink } from '../views/docs/prose';

export const SET_TASK_SIG_PY = `call.set_task( # Unique identifier for this task, used to bind on_task_complete handlers. task_id: str,

High-level goal for the agent. Provides context when no checklist is given,

# or alongside a checklist to frame the overall objective.
objective: str = "",

Ordered list of items for the agent to complete during the call.

checklist: list[Field | Say | str] | None = None,

Optional extra guidance on when to consider this task done. Useful for

# open-ended tasks where the checklist alone doesn't define completion.
completion_criteria: str = "",

)`;

export const SET_TASK_SIG_TS = `await call.setTask({ // Unique identifier for this task, used to bind onTaskComplete handlers. taskId: string,

// High-level goal for the agent. Provides context when no checklist is given, // or alongside a checklist to frame the overall objective. objective?: string,

// Ordered list of items for the agent to complete during the call. checklist?: (FieldItem | SayItem | string)[], })`;

Task

A task is the unit of work your agent completes on a call. Call call.set_task() to direct the agent toward a new goal. You can invoke call.set_task() on one of your handler callbacks, or at any time (even on another thread).

<CodeTabs python={{ code: SET_TASK_SIG_PY, filename: "signature" }} typescript={{ code: SET_TASK_SIG_TS, filename: "signature" }} />

Checklist items

The checklist drives the agent forward. Each item is one of three types:

Type Purpose
guava.Field Collect structured data from the caller
guava.Say Speak a verbatim statement
str Natural language instruction for the agent
guava.Say A guava.Say step is spoken verbatim — use it sparingly when exact wording matters.

Example

export const SET_TASK_EX_PY = `@agent.on_call_start def on_call_start(call: guava.Call): call.set_task( "waitlist", objective="You are a virtual assistant for Thai Palace. Add callers to the waitlist.", checklist=[ guava.Field(key="caller_name", field_type="text", description="Name for the waitlist"), guava.Field(key="party_size", field_type="integer", description="Number of people"), 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 confirm.", ], )

@agent.on_task_complete("waitlist") def on_waitlist_done(call: guava.Call): logger.info("Added %s, party of %d, to waitlist.", call.get_field("caller_name"), call.get_field("party_size")) call.hangup("Thank the caller and let them know we'll text when their table is ready.")`;

export const SET_TASK_EX_TS = `agent.onCallStart(async (call) => { await call.setTask({ taskId: "waitlist", objective: "You are a virtual assistant for Thai Palace. Add callers to the waitlist.", checklist: [ guava.Field({ key: "caller_name", fieldType: "text", description: "Name for the waitlist" }), guava.Field({ key: "party_size", fieldType: "integer", description: "Number of people" }), guava.Field({ key: "phone_number", fieldType: "text", description: "Phone number to text when the table is ready", }), "Read the phone number back to the caller to confirm.", ], }); });

agent.onTaskComplete("waitlist", async (call) => { logger.info("Added %s, party of %d, to waitlist.", await call.getField("caller_name"), await call.getField("party_size")); await call.hangup("Thank the caller and let them know we'll text when their table is ready."); });`;

<CodeTabs python={{ code: SET_TASK_EX_PY, filename: "example.py" }} typescript={{ code: SET_TASK_EX_TS, filename: "example.ts" }} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink, PropTable } from '../views/docs/prose';

export const FIELD_SIG_PY = `guava.Field( # Identifier used to retrieve the value via get_field() after collection. key: str,

Natural-language instruction to the LLM about how to collect this value.

# Use when you do not particularly care how the agent phrases its question.
description: str = '',

Encourages the agent to ask for the field in a particular way. Use instead

# of description when you want more control over the phrasing.
question: str = '',

Controls parsing and validation. "calendar_slot" and "multiple_choice"

# require either choices or searchable=True.
field_type: Literal[
    'text', 'date', 'datetime', 'integer', 'multiple_choice', 'calendar_slot'
] = 'text',

If False, the agent can skip this field if the caller is unwilling to provide it.

required: bool = True,

Static list of valid options for "calendar_slot" and "multiple_choice" fields.

# Use when the list is small. Large lists should use searchable=True.
choices: list[str] = [],

When True, enables dynamic search for "multiple_choice" and "calendar_slot"

# fields. The agent searches for options matching the caller's query at runtime.
searchable: bool = False,

)`;

export const FIELD_SIG_TS = `guava.Field({ // Identifier used to retrieve the value via get_field() after collection. key: string,

// Natural-language instruction to the LLM about how to collect this value. // Use when you do not particularly care how the agent phrases its question. description: string,

// Controls parsing and validation. "calendar_slot" and "multiple_choice" // require either choices or choiceGenerator. fieldType: 'text' | 'date' | 'datetime' | 'integer' | 'multiple_choice' | 'calendar_slot',

// If false, the agent can skip this field if the caller is unwilling to provide it. required?: boolean, // default: true

// Static list of valid options for "calendar_slot" and "multiple_choice" fields. // Use when the list is small. Large lists should use choiceGenerator. choices?: string[], // default: []

// Takes a query string and returns (matching, fallback) lists. Use for large // or dynamic option sets with "calendar_slot" and "multiple_choice". choiceGenerator?: ChoiceGenerator,

// When true, enables dynamic search for "multiple_choice" and "calendar_slot" // fields. The agent searches for options matching the caller's query at runtime. searchable?: boolean, // default: false })`;

Field

A Field is a Task checklist item instructing the Guava agent to collect structured data from the caller. The agent elicits the value through natural conversation, validates it against the specified type, and marks the checklist item complete when satisfied.

<CodeTabs python={{ code: FIELD_SIG_PY, filename: "signature" }} typescript={{ code: FIELD_SIG_TS, filename: "signature" }} />

Basic Examples

export const FIELD_EX1_PY = `# Basic text field field = guava.Field( key="caller_name", description="Get the caller's name", )

Integer field with question

field = guava.Field( key="caller_age", question="How old are you?", field_type="integer", )

Multiple choice with static choices

field = guava.Field( key="caller_preference", description="Get the caller's preferred fruit", field_type="multiple_choice", # Use searchable=True instead when there's a large number of choices choices=["apple", "banana", "orange"], required=False, )`;

export const FIELD_EX1_TS = `// Basic text field const field = guava.Field({ key: "caller_name", description: "Get the caller's name", });

// Integer field with question const field = guava.Field({ key: "caller_age", description: "How old are you?", fieldType: "integer", });

// Multiple choice with static choices const field = guava.Field({ key: "caller_preference", description: "Get the caller's preferred fruit", fieldType: "multiple_choice", // Use searchable: true instead when there's a large number of choices choices: ["apple", "banana", "orange"], required: false, });`;

<CodeTabs python={{ code: FIELD_EX1_PY, filename: "examples.py" }} typescript={{ code: FIELD_EX1_TS, filename: "examples.ts" }} />

Search Fields

Some fields can have a very large set of valid options. For example, a destination_airport field may include thousands of airports worldwide. In other cases, options must be generated dynamically, such as an appointment_time field populated from a booking system.

This is where search fields come in handy. Set searchable=True on the field, then register an @agent.on_search_query handler. When the agent needs options, it calls your handler with a natural-language query string. Return two lists: a primary list of matches, and a fallback list shown only when no primary matches are found.

export const FIELD_EX4_PY = `field = guava.Field( key="airport", description="Find a suitable airport for the caller", field_type="multiple_choice", searchable=True, )

@agent.on_search_query("airport") def search_airports(call: guava.Call, query: str): matching_airports: list[str] = [] other_airports: list[str] = []

... # Do some work to generate a few matching airport # options based on the caller's query. # 'query' will be human natural language # (e.g. "I need to fly out of an airport in # southern california") ...

The second list only becomes relevant if there

# are no matches to the caller's query. It is used
# to at least present something to the caller in
# case there are no perfect matches.
return matching_airports, other_airports`;

export const FIELD_EX4_TS = `const field = guava.Field({ key: "airport", description: "Find a suitable airport for the caller", fieldType: "multiple_choice", searchable: true, });

agent.onSearchQuery("airport", async (call, query) => { const matchingAirports: string[] = []; const otherAirports: string[] = [];

// ... // Do some work to generate a few matching airport // options based on the caller's query. // 'query' will be human natural language // (e.g. "I need to fly out of an airport in // southern california") // ...

// The second list only becomes relevant if there // are no matches to the caller's query. It is used // to at least present something to the caller in // case there are no perfect matches. return [matchingAirports, otherAirports]; })`;

<CodeTabs python={{ code: FIELD_EX4_PY, filename: "search_field.py" }} typescript={{ code: FIELD_EX4_TS, filename: "search_field.ts" }} />

Field Types Reference

Type Example collected value Return type from get_field()
text "I want to cancel my appointment" str
date {"year": 2024, "month": 3, "day": 15} dict with keys year, month, day (all int)
integer 42 int
multiple_choice "apple" str (guaranteed to be one of choices or returned by choice_generator)
calendar_slot "2022-12-31T17:30" ISO-8601 datetime str
Note: The `choices` list for `calendar_slot` fields must be ISO-8601 datetimes (e.g. `"2022-12-31T17:30"`).

import { CodeBlock } from '../views/docs/CodeBlock'; import { PropTable, NextLink } from '../views/docs/prose';

export const RUNNER_EX = `import os from guava import Agent, Runner

agent_a = Agent(name="Grace", purpose="You are a helpful voice agent.") agent_b = Agent(name="Jordan", purpose="You are a helpful voice agent.")

runner = Runner() runner.listen_phone(agent_a, os.environ["GUAVA_AGENT_NUMBER"]) runner.listen_webrtc(agent_b) runner.run()`;

Runner

guava.Runner lets you serve multiple agents in a single process. Each agent can be attached to any number of channels — phone, WebRTC, SIP, or outbound campaigns. Call run() to start everything and block until all channels exit.

Methods

<PropTable rows={[ { name: "listen_phone(agent, agent_number)", type: "Runner", desc: "Register an agent to receive inbound calls on the given phone number. Returns self for chaining." }, { name: "listen_webrtc(agent, webrtc_code=None)", type: "Runner", desc: "Register an agent to accept WebRTC connections. A new code is generated if webrtc_code is omitted. Returns self for chaining." }, { name: "listen_sip(agent, sip_code)", type: "Runner", desc: "Register an agent to receive inbound SIP calls on the given SIP code. Returns self for chaining." }, { name: "attach_campaign(agent, campaign)", type: "Runner", desc: "Attach an outbound campaign to an agent. Returns self for chaining." }, { name: "run()", type: "None", desc: "Start all registered channels in daemon threads and block until they all exit." }, ]} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { PropTable, NextLink } from '../views/docs/prose';

export const CLIENT_EX_PY = `import guava, os

client = guava.Client( api_key=os.environ["GUAVA_API_KEY"], # or omit — reads env automatically )`;

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

const client = new guava.Client( process.env.GUAVA_API_KEY, // or omit — reads env automatically );`;

Client

guava.Client complements guava.Agent by providing functions for managing account-level resources. It also provides any functions that don't fit onto guava.Agent.

<CodeTabs python={{ code: CLIENT_EX_PY, filename: "run.py" }} typescript={{ code: CLIENT_EX_TS, filename: "run.ts" }} />

API

<PropTable rows={[ { name: "api_key", type: "str | None", default: "env GUAVA_API_KEY", desc: "Your Guava API key." }, { name: "base_url", type: "str | None", default: "production", desc: "Override the API endpoint (for testing)." }, ]} />

Methods

<PropTable rows={[ { name: "create_sip_agent()", type: "method", desc: "Generate a SIP code linked to your account for inbound SIP call handling." }, { name: "create_webrtc_agent(ttl)", type: "method", desc: "Generate a WebRTC code for browser-based voice interaction, with optional TTL." }, { name: "send_sms()", type: "method", desc: "Send an SMS message. See SMS Messaging." }, { name: "next_sms()", type: "method", desc: "Wait for and return the next inbound SMS reply. See SMS Messaging." }, ]} />

See SMS Messaging for details on send_sms and next_sms.


import { CodeTabs } from '../views/docs/CodeTabs'; import { PropTable, Callout, NextLink } from '../views/docs/prose';

export const SMS_EX_PY = `import guava import os

client = guava.Client(api_key=os.environ["GUAVA_API_KEY"])

agent_number = os.environ["GUAVA_AGENT_NUMBER"] # one of your Guava numbers customer = "+15551234567"

Send an SMS from your Guava number to the customer.

client.send_sms( from_number=agent_number, to_number=customer, message="Hi! Reply YES to confirm your appointment, or STOP to opt out.", )

Block until the customer replies, giving up after 5 minutes.

reply = client.next_sms(from_number=customer, to_number=agent_number, timeout=300)

if reply is None: print("No reply within 5 minutes.") else: print("Customer replied:", reply["content"])`;

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

const client = new guava.Client(process.env.GUAVA_API_KEY);

const agentNumber = process.env.GUAVA_AGENT_NUMBER; // one of your Guava numbers const customer = "+15551234567";

// Send an SMS from your Guava number to the customer. await client.sendSms( agentNumber, customer, "Hi! Reply YES to confirm your appointment, or STOP to opt out.", );

// Block until the customer replies, giving up after 5 minutes. const reply = await client.nextSms(customer, agentNumber, { timeoutMs: 300_000 });

if (reply === null) { console.log("No reply within 5 minutes."); } else { console.log("Customer replied:", reply.content); }`;

export const SMS_SEND_SIG_PY = client.send_sms( from_number: str, to_number: str, message: str, ) -> None;

export const SMS_SEND_SIG_TS = await client.sendSms( fromNumber: string, toNumber: string, message: string, ): Promise<void>;

export const SMS_NEXT_SIG_PY = client.next_sms( from_number: str, to_number: str, *, timeout: float = 60.0, poll_interval: float = 2.0, ) -> dict | None;

export const SMS_NEXT_SIG_TS = await client.nextSms( fromNumber: string, toNumber: string, options?: { timeoutMs?: number; pollIntervalMs?: number }, ): Promise<SmsMessage | null>;

SMS Messaging

The guava.Client can send SMS messages from your Guava numbers and wait for inbound replies. This is useful for sending confirmations and reminders, or for collecting a response between calls.

SMS messaging is available in the Python and TypeScript SDKs. For other languages, call the equivalent Messages REST API directly.

<CodeTabs python={{ code: SMS_EX_PY, filename: "sms.py" }} typescript={{ code: SMS_EX_TS, filename: "sms.ts" }} />

send_sms / sendSms

Send a single SMS message. The from_number must be one of your Guava numbers with SMS configured, and the message is delivered to to_number.

<CodeTabs python={{ code: SMS_SEND_SIG_PY, filename: "signature" }} typescript={{ code: SMS_SEND_SIG_TS, filename: "signature" }} />

<PropTable rows={[ { name: "from_number", type: "str", desc: "One of your Guava numbers, in E.164 format (e.g. "+15551230001"). Must have SMS enabled." }, { name: "to_number", type: "str", desc: "The recipient's number, in E.164 format." }, { name: "message", type: "str", desc: "The message body to send." }, ]} />

Returns nothing (Python None; TypeScript resolves void). Raises (Python) / rejects (TypeScript) if the from_number isn't owned by your organization or doesn't have SMS configured.

Sending SMS requires your organization to complete SMS brand and campaign registration. See Outbound & SMS Permissions.

next_sms / nextSms

Wait for the next inbound SMS sent to one of your Guava numbers from a given number, and return it. next_sms polls your inbox and only returns messages received after the call begins, so a reply to an earlier message won't be returned twice.

Note the direction: `from_number` is the external number you're waiting to hear from (the customer), and `to_number` is your Guava number that receives the reply — the opposite of send_sms.

<CodeTabs python={{ code: SMS_NEXT_SIG_PY, filename: "signature" }} typescript={{ code: SMS_NEXT_SIG_TS, filename: "signature" }} />

<PropTable rows={[ { name: "from_number", type: "str", desc: "The external number you're waiting to hear from, in E.164 format." }, { name: "to_number", type: "str", desc: "Your Guava number that will receive the reply, in E.164 format." }, { name: "timeout", type: "float", default: "60.0", desc: "Maximum number of seconds to wait before giving up. In TypeScript, pass timeoutMs (milliseconds) in the options object; defaults to 60000." }, { name: "poll_interval", type: "float", default: "2.0", desc: "Seconds to wait between inbox checks. In TypeScript, pass pollIntervalMs (milliseconds) in the options object; defaults to 2000." }, ]} />

Returns the message (Python dict / TypeScript SmsMessage), or None / null if the timeout elapses with no new message. A message has the following fields:

<PropTable rows={[ { name: "id", type: "str", desc: "Unique ID of the message." }, { name: "from_number", type: "str", desc: "The number that sent the message." }, { name: "to_number", type: "str", desc: "Your Guava number that received the message." }, { name: "content", type: "str", desc: "The message body." }, { name: "received_at", type: "str", desc: "When the message was received, in ISO 8601 format." }, { name: "modality", type: "str", desc: "The channel the message arrived on. Currently always "sms"." }, { name: "direction", type: "str", desc: "Always "inbound" for received messages." }, ]} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { NextLink } from '../views/docs/prose';

export const ON_QUESTION_SIG_PY = @agent.on_question def on_question(call: guava.Call, question: str) -> str: # question: natural-language question from the caller # return: answer to relay to caller ...;

export const ON_QUESTION_SIG_TS = agent.onQuestion(async (call: guava.Call, question: string) => string);;

export const ON_QUESTION_EX_PY = `import guava from guava import Agent from guava.helpers.rag import DocumentQA from guava.examples.example_data import PROPERTY_INSURANCE_POLICY

agent = Agent( organization="Harper Valley Property Insurance", purpose="Answer questions regarding property insurance policy", )

document_qa = DocumentQA(documents=PROPERTY_INSURANCE_POLICY)

@agent.on_question def on_question(call: guava.Call, question: str) -> str: return document_qa.ask(question)`;

export const ON_QUESTION_EX_TS = `import * as guava from "@guava-ai/guava-sdk"; import { DocumentQA } from "@guava-ai/guava-sdk/helpers/openai"; import { PROPERTY_INSURANCE_POLICY } from "@guava-ai/guava-sdk/example-data";

const agent = new guava.Agent({ organization: "Harper Valley Property Insurance", purpose: "Answer questions regarding property insurance policy", });

const documentQA = new DocumentQA( "harper-valley-property-insurance", PROPERTY_INSURANCE_POLICY, );

agent.onQuestion(async (call: guava.Call, question: string) => { return await documentQA.ask(question); });`;

on_question()

When a Guava agent is asked a question that it cannot answer from its context alone, it will invoke the on_question callback. Your Expert then has the chance to answer that question, typically using a RAG system. Our examples use the provided DocumentQA class, but you can use any RAG system you prefer.

See our Q&A example for a step-by-step walkthrough.

<CodeTabs python={{ code: ON_QUESTION_SIG_PY, filename: "signature" }} typescript={{ code: ON_QUESTION_SIG_TS, filename: "signature" }} />

If you want the agent to answer questions immediately, use add_info to pre-emptively add information to the context.

  • on_question, like all Guava callbacks, is invoked asynchronously and does not block dialog. The Guava voice agent continues to engage the caller until the question answer comes back.
  • on_question may be invoked multiple times, for example, if a caller asks a question and then refines it. on_question may be invoked speculatively before a caller is done talking.
  • on_question may be invoked simultaneously with on_action_request, as some requests can be both an "action" and a "question". For example, "Do you have a lost and found?" In this case, the agent will synthesize both responses: "Yes, we have a lost and found. Would you like me to transfer you there?"

Example

<CodeTabs python={{ code: ON_QUESTION_EX_PY, filename: "support_controller.py" }} typescript={{ code: ON_QUESTION_EX_TS, filename: "support_controller.ts" }} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink, PropTable } from '../views/docs/prose';

export const ON_ACTION_REQUEST_SIG_PY = `@agent.on_action_request def on_action_request(call: guava.Call, request: str) -> SuggestedAction | None: # request: natural-language summary of what the caller wants # return: SuggestedAction(key=...) or None

@agent.on_action("action_key") def handler(call: guava.Call) -> None: # Runs when Guava executes the action with the matching key ...`;

export const ON_ACTION_REQUEST_SIG_TS = `agent.onActionRequest( async (call: guava.Call, request: string) => { key: string } | null );

agent.onAction("action_key", async (call: guava.Call) => { // Runs when Guava executes the action with the matching key });`;

export const ON_ACTION_REQUEST_EX_PY = `from guava import Agent, SuggestedAction from guava.helpers.openai import IntentRecognizer

agent = Agent(name="Nova", organization="Thai Palace", purpose="...")

ACTIONS = { "reservation": "for handling reservations", "waitlist": "additions to the waitlist", "delivery": "for takeout orders", "hiring": "for people looking for jobs", "order_for_pickup": "", }

intent_recognizer = IntentRecognizer(ACTIONS)

@agent.on_action_request def on_action_request(call: guava.Call, request: str) -> SuggestedAction | None: key = intent_recognizer.classify(request) return SuggestedAction(key=key) if key else None

@agent.on_action("reservation") def reservation(call: guava.Call): call.set_task(...)

@agent.on_action("waitlist") def waitlist(call: guava.Call): call.set_task(...)`;

export const ON_ACTION_REQUEST_EX_TS = `import * as guava from "@guava-ai/guava-sdk"; import { IntentRecognizer } from "@guava-ai/guava-sdk/helpers/openai";

const agent = new guava.Agent({ name: "Nova", organization: "Thai Palace", purpose: "...", });

const ACTIONS = { reservation: "for handling reservations", waitlist: "additions to the waitlist", delivery: "for takeout orders", hiring: "for people looking for jobs", order_for_pickup: "", };

const intentRecognizer = new IntentRecognizer(Object.keys(ACTIONS));

agent.onActionRequest(async (_call: guava.Call, request: string) => { const key = await intentRecognizer.classify(request); return key ? { key } : null; });

agent.onAction("reservation", async (call: guava.Call) => { call.setTask({ objective: "Handle reservation" }); });

agent.onAction("waitlist", async (call: guava.Call) => { call.setTask({ objective: "Handle waitlist addition" }); });`;

on_action_request() / on_action()

These handlers are used when the caller expresses an intent or action (e.g. "I'd like to pay my bill"). The flow is as follows.

  1. The caller makes a request — e.g. "I'd like to check the status of my order."
  2. Guava invokes on_action_request with a summary of the request — e.g. "the customer would like to check the status of their order."
  3. You classify the request and return a SuggestedAction — e.g. SuggestedAction(key="order_status"). You can use our built-in IntentRecognizer helper, or build your own intent classifier. Return None if no action matches the request.
  4. Guava decides whether to execute the action — it may proceed immediately or ask the caller to confirm.
  5. Guava executes the action — The on_action handler registered under the matching suggested action key is called.
Design note: The two-step pattern (request → execute) gives the agent a chance to confirm intent with the caller before committing to an action.

<CodeTabs python={{ code: ON_ACTION_REQUEST_SIG_PY, filename: "signature" }} typescript={{ code: ON_ACTION_REQUEST_SIG_TS, filename: "signature" }} />

Interaction with on_question

A caller utterance can be both a question and an action (e.g. "Could you help me pay my bill?"). In this case Guava will invoke both callbacks in parallel and synthesize an appropriate response based on the results.

For example, if on_question returns "Yes, we handle bill payment" and on_action_request returns SuggestedAction(key="bill_pay"), Guava may immediately chain into executing the action, or it may respond "Yes — would you like to get started?" to confirm the action with the caller.

Example

<CodeTabs python={{ code: ON_ACTION_REQUEST_EX_PY, filename: "restaurant_controller.py" }} typescript={{ code: ON_ACTION_REQUEST_EX_TS, filename: "restaurant_controller.ts" }} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink } from '../views/docs/prose';

export const ON_TASK_COMPLETE_SIG_PY = `# Per-task form (recommended) @agent.on_task_complete("task_name") def handler(call: guava.Call) -> None: ...

Generic form — fires for all tasks

@agent.on_task_complete def handler(call: guava.Call, task_id: str) -> None: ...`;

export const ON_TASK_COMPLETE_SIG_TS = `// Per-task form (recommended) agent.onTaskComplete("task_name", async (call: guava.Call) => void);

// Generic form — fires for all tasks agent.onTaskComplete(async (call: guava.Call, taskId: string) => void);`;

export const ON_TASK_COMPLETE_EX_PY = `import logging import guava from guava import Agent

logger = logging.getLogger(name)

agent = Agent( organization="Thai Palace", purpose="Add callers to the waitlist", )

@agent.on_call_start def on_call_start(call: guava.Call): call.set_task( "waitlist", objective="Add the caller to the waitlist.", checklist=[ guava.Field(key="caller_name", field_type="text", description="Name for the waitlist"), guava.Field(key="party_size", field_type="integer", description="Number of people"), guava.Field(key="phone_number", field_type="text", description="Phone number to text when ready"), "Read the phone number back to the caller to confirm.", ], )

@agent.on_task_complete("waitlist") def on_waitlist_done(call: guava.Call): name = call.get_field("caller_name") size = call.get_field("party_size") logger.info("Added %s, party of %d, to waitlist.", name, size) call.hangup("Thank the caller and let them know we'll text when their table is ready.")`;

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

const agent = new guava.Agent({ organization: "Thai Palace", purpose: "Add callers to the waitlist", });

agent.onCallStart(async (call) => { await call.setTask({ taskId: "waitlist", objective: "Add the caller to the waitlist.", checklist: [ guava.Field({ key: "caller_name", fieldType: "text", description: "Name for the waitlist" }), guava.Field({ key: "party_size", fieldType: "integer", description: "Number of people" }), guava.Field({ key: "phone_number", fieldType: "text", description: "Phone number to text when ready" }), "Read the phone number back to the caller to confirm.", ], }); });

agent.onTaskComplete("waitlist", async (call) => { const name = await call.getField("caller_name"); const size = await call.getField("party_size"); console.log(`Added ${name}, party of ${size}, to waitlist.`); await call.hangup("Thank the caller and let them know we'll text when their table is ready."); });`;

on_task_complete()

on_task_complete is called when a Task previously set with call.set_task() is completed by the agent. Use it to persist collected data, trigger downstream workflows, or move the call to the next stage.

Signature

There are two forms:

<CodeTabs python={{ code: ON_TASK_COMPLETE_SIG_PY, filename: "signature" }} typescript={{ code: ON_TASK_COMPLETE_SIG_TS, filename: "signature" }} />

  • Per-task form (recommended): @agent.on_task_complete("task_name") binds the handler to a specific task_id. The handler receives only the Call object.
  • Generic form: @agent.on_task_complete (bare decorator) fires for every completed task. The handler receives the Call object and the task_id string, letting you dispatch on it manually.
You cannot mix both forms on the same agent — using per-task handlers alongside a generic handler raises a TypeError.
  • on_task_complete fires once all checklist items are resolved and the agent has signaled completion.
  • Use call.get_field() inside the handler to read values collected during the task.
  • The call is still live when this handler runs — you can issue commands such as call.set_task(), call.hangup(), or call.transfer().

Example

<CodeTabs python={{ code: ON_TASK_COMPLETE_EX_PY, filename: "waitlist_controller.py" }} typescript={{ code: ON_TASK_COMPLETE_EX_TS, filename: "waitlist_controller.ts" }} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink, PropTable } from '../views/docs/prose'; export const ON_DTMF_SIG_PY = @agent.on_dtmf def on_dtmf(call: guava.Call, event: DTMFPressedEvent) -> None: ...;

export const ON_DTMF_SIG_TS = agent.onDtmf((call: guava.Call, event: DTMFPressedEvent) => Promise<void>);;

on_dtmf()

Register a handler that fires whenever the caller presses a DTMF digit (0–9, *, #, A–D) on their keypad.

Caller keypresses only. on_dtmf fires for digits pressed by the caller. To enable the agent itself to send DTMF tones (e.g. to navigate an IVR system it has called into), use call.set_agent_dtmf(enabled=True) instead.

The DTMFPressedEvent is a pydantic model imported from guava.events:

from guava.events import DTMFPressedEvent

class DTMFPressedEvent(BaseEvent):
    event_type: Literal["dtmf"] = "dtmf"
    digit: Literal["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#", "A", "B", "C", "D"]

Signature

<CodeTabs python={{ code: ON_DTMF_SIG_PY, filename: "signature" }} typescript={{ code: ON_DTMF_SIG_TS, filename: "signature" }} />

<PropTable rows={[ { name: "call", type: "Call", desc: "The active call object.", }, { name: "event", type: "DTMFPressedEvent", desc: 'Contains digit (string): the key the caller pressed — one of "0"–"9", "*", "#", "A", "B", "C", or "D".', }, ]} />

Return value: None


import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink, PropTable } from '../views/docs/prose';

export const ON_SESSION_END_SIG_PY = @agent.on_session_end def on_session_end(call: guava.Call, event: BotSessionEnded) -> None: ...;

export const ON_SESSION_END_SIG_TS = agent.onSessionEnd(async (call: guava.Call, event: BotSessionEnded) => void);;

export const ON_SESSION_END_EX_PY = `import logging from guava.events import BotSessionEnded

logger = logging.getLogger(name)

@agent.on_session_end def on_session_end(call: guava.Call, event: BotSessionEnded): logger.info("session ended: reason=%s", event.termination_reason) if event.termination_reason == "user-hangup": # caller hung up — save any collected data ... elif event.termination_reason == "bot-transfer": # call was transferred to a human agent ...`;

export const ON_SESSION_END_EX_TS = `import * as guava from "@guava-ai/guava-sdk"; import type { BotSessionEnded } from "@guava-ai/guava-sdk";

agent.onSessionEnd(async (_call: guava.Call, event: BotSessionEnded) => { console.log("session ended:", event.termination_reason); if (event.termination_reason === "user-hangup") { // caller hung up — save any collected data } else if (event.termination_reason === "bot-transfer") { // call was transferred to a human agent } });`;

on_session_end()

Register a handler that fires when a call session ends. Use this to save call data, trigger post-call workflows, or log outcomes.

The BotSessionEnded event carries a termination_reason field that tells you why the session ended:

Value Meaning
"user-hangup" The caller hung up.
"bot-hangup" The agent ended the call (e.g. via call.hangup()).
"bot-failure" The session ended due to an internal error.
"bot-transfer" The call was transferred to another destination.
"voicemail" The outbound call reached voicemail.

Signature

<CodeTabs python={{ code: ON_SESSION_END_SIG_PY, filename: "signature" }} typescript={{ code: ON_SESSION_END_SIG_TS, filename: "signature" }} />

<PropTable rows={[ { name: "call", type: "Call", desc: "The call object. Note: the call is already ended — do not issue commands on it.", }, { name: "event", type: "BotSessionEnded", desc: "Contains termination_reason — one of \"user-hangup\", \"bot-hangup\", \"bot-failure\", \"bot-transfer\", \"voicemail\".", }, ]} />

Return value: None

Example

<CodeTabs python={{ code: ON_SESSION_END_EX_PY, filename: "controller.py" }} typescript={{ code: ON_SESSION_END_EX_TS, filename: "controller.ts" }} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink, PropTable } from '../views/docs/prose'; export const ON_AGENT_SPEECH_SIG_PY = @agent.on_agent_speech def on_agent_speech(call: guava.Call, event: AgentSpeechEvent) -> None: ...;

export const ON_AGENT_SPEECH_SIG_TS = agent.onAgentSpeech((call: guava.Call, event: AgentSpeechEvent) => void);;

export const ON_AGENT_SPEECH_EX_PY = `import logging from guava.events import AgentSpeechEvent

logger = logging.getLogger(name)

@agent.on_agent_speech def on_agent_speech(call: guava.Call, event: AgentSpeechEvent): logger.info("agent speech event: %s", event)

Output:

[INFO 15:02:29] agent speech event: sequence=None event_type='agent-speech'

utterance='Hi, thank you for calling Thai Palace. My name is Grace.

I can help you with the waitlist. ' interrupted=False`;

export const ON_AGENT_SPEECH_EX_TS = `import * as guava from "@guava-ai/guava-sdk"; import { AgentSpeechEvent } from "@guava-ai/guava-sdk/events";

agent.onAgentSpeech((_call: guava.Call, event: AgentSpeechEvent) => { console.log("agent speech event:", JSON.stringify(event)); });`;

on_agent_speech()

Rarely needed. This callback fires for every utterance spoken by the agent. Most implementations will not need it and should instead rely on higher-level callbacks like on_question or on_task_complete. It is most useful for implementing call surveillance or real-time transcription logging.

Register a handler to receive a callback whenever the agent speaks. The event contains what the agent said and whether it was interrupted by the caller.

The AgentSpeechEvent is a pydantic model imported from guava.events:

from guava.events import AgentSpeechEvent

class AgentSpeechEvent(BaseEvent):
    event_type: Literal["agent-speech"] = "agent-speech"
    utterance: str
    interrupted: bool = False

Signature

<CodeTabs python={{ code: ON_AGENT_SPEECH_SIG_PY, filename: "signature" }} typescript={{ code: ON_AGENT_SPEECH_SIG_TS, filename: "signature" }} />

<PropTable rows={[ { name: "call", type: "Call", desc: "The active call object.", }, { name: "event", type: "AgentSpeechEvent", desc: "Contains utterance (string) and interrupted (boolean) fields.", }, ]} />

Return value: None

Example

<CodeTabs python={{ code: ON_AGENT_SPEECH_EX_PY, filename: "controller.py" }} typescript={{ code: ON_AGENT_SPEECH_EX_TS, filename: "controller.ts" }} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink, PropTable } from '../views/docs/prose'; export const ON_CALLER_SPEECH_SIG_PY = @agent.on_caller_speech def on_caller_speech(call: guava.Call, event: CallerSpeechEvent) -> None: ...;

export const ON_CALLER_SPEECH_SIG_TS = agent.onCallerSpeech((call: guava.Call, event: CallerSpeechEvent) => void);;

export const ON_CALLER_SPEECH_EX_PY = `import logging from guava.events import CallerSpeechEvent

logger = logging.getLogger(name)

@agent.on_caller_speech def on_caller_speech(call: guava.Call, event: CallerSpeechEvent): logger.info("caller speech event: %s", event)

Output:

[INFO 13:30:43] caller speech event: sequence=None event_type='caller-speech'

utterance='Hi Grace.' utterance_id='19d92d6c68b'

[INFO 13:30:45] caller speech event: sequence=None event_type='caller-speech'

utterance='Hi Grace. I am looking' utterance_id='19d92d6c68b'

[INFO 13:30:46] caller speech event: sequence=None event_type='caller-speech'

utterance='Hi Grace. I am looking to make a reservation' utterance_id='19d92d6c68b'

[INFO 13:30:49] caller speech event: sequence=None event_type='caller-speech'

utterance="It's for me" utterance_id='19d92d6deec'

[INFO 13:30:50] caller speech event: sequence=None event_type='caller-speech'

utterance="It is for me and a couple friends." utterance_id='19d92d6deec'`;

export const ON_CALLER_SPEECH_EX_TS = `import * as guava from "@guava-ai/guava-sdk"; import { CallerSpeechEvent } from "@guava-ai/guava-sdk/events";

agent.onCallerSpeech((_call: guava.Call, event: CallerSpeechEvent) => { console.log("caller speech event:", JSON.stringify(event)); });`;

on_caller_speech()

Rarely needed. This callback fires for every utterance spoken by the caller. Most implementations will not need it and should instead rely on higher-level callbacks like on_question or on_task_complete. It is most useful for implementing call surveillance or real-time transcription logging.

Register a handler to receive a callback whenever caller speech is detected. The event contains what the caller said and an utterance_id that distinguishes new utterances from updates to existing ones.

As transcription progresses, you may receive multiple events with the same utterance_id. Usually these updates append new words, but there can be slight corrections to previously transcribed words. For example:

  • "Hi." — utterance_id='0'
  • "I am going to the store" — utterance_id='1'
  • "I'm going to the store and" — utterance_id='1' (update to the same utterance)

The CallerSpeechEvent is a pydantic model imported from guava.events:

from guava.events import CallerSpeechEvent

class CallerSpeechEvent(BaseEvent):
    event_type: Literal["caller-speech"] = "caller-speech"
    utterance: str
    utterance_id: Optional[str] = None

Signature

<CodeTabs python={{ code: ON_CALLER_SPEECH_SIG_PY, filename: "signature" }} typescript={{ code: ON_CALLER_SPEECH_SIG_TS, filename: "signature" }} />

<PropTable rows={[ { name: "call", type: "Call", desc: "The active call object.", }, { name: "event", type: "CallerSpeechEvent", desc: "Contains utterance (string) and utterance_id (optional string) fields.", }, ]} />

Return value: None

Example

<CodeTabs python={{ code: ON_CALLER_SPEECH_EX_PY, filename: "controller.py" }} typescript={{ code: ON_CALLER_SPEECH_EX_TS, filename: "controller.ts" }} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink } from '../views/docs/prose';

set_task()

call.set_task() directs the agent toward a new goal mid-call. It accepts an objective, an ordered checklist of steps, and a task ID for binding completion handlers.

export const SET_TASK_SIG_PY = call.set_task( task_id: str, objective: str = "", checklist: list[Field | Say | str] | None = None, completion_criteria: str = "", );

export const SET_TASK_SIG_TS = await call.setTask({ taskId: string, objective?: string, checklist?: (FieldItem | SayItem | string)[], });

<CodeTabs python={{ code: SET_TASK_SIG_PY, filename: "signature" }} typescript={{ code: SET_TASK_SIG_TS, filename: "signature" }} />

Full reference: See the Task page for parameter details, checklist item types, and a complete example.

import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink, PropTable } from '../views/docs/prose'; export const SET_PERSONA_SIG_PY = call.set_persona( organization_name: str | None = None, agent_name: str | None = None, agent_purpose: str | None = None, voice: str | None = None, );

export const SET_PERSONA_SIG_TS = await call.setPersona({ organizationName?: string, agentName?: string, agentPurpose?: string, voice?: string, });

export const SET_PERSONA_EX_PY = call.set_persona( organization_name="Bright Smile Dental", agent_name="Alex", agent_purpose="You are calling patients to help them schedule and confirm dental appointments", voice="grace", );

export const SET_PERSONA_EX_TS = await call.setPersona({ organizationName: "Bright Smile Dental", agentName: "Alex", agentPurpose: "Help patients schedule dental appointments", });

set_persona()

call.set_persona() is deliberately minimal. Give it the organization name and let the agent figure out the tone. You don't need to specify a voice style, a greeting template, or a list of prohibited phrases — Guava's defaults are professional and natural.

<CodeTabs python={{ code: SET_PERSONA_SIG_PY, filename: "signature" }} typescript={{ code: SET_PERSONA_SIG_TS, filename: "signature" }} />

<PropTable rows={[ { name: "organization_name", type: "str | None", desc: "The organization the agent represents. Used in introductions.", }, { name: "agent_name", type: "str | None", desc: "The agent's first name. Defaults to a generic 'assistant' style.", }, { name: "agent_purpose", type: "str | None", desc: "A sentence describing why the agent is calling. Sets the LLM's operating context.", }, { name: "voice", type: "str | None", default: '"grace"', desc: 'The TTS voice to use. Options: "grace" (southern female) or "jack" (British male). For languages outside of English, only the "grace" voice is supported.', }, ]} />

Example

<CodeTabs python={{ code: SET_PERSONA_EX_PY, filename: "example.py" }} typescript={{ code: SET_PERSONA_EX_TS, filename: "example.ts" }} />

Tip: Include the contact's name in `agent_purpose` to help the agent personalize the conversation naturally.

import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink, PropTable } from '../views/docs/prose'; export const SEND_INSTRUCTION_SIG_PY = call.send_instruction(instruction: str) -> None;

export const SEND_INSTRUCTION_SIG_TS = await call.sendInstruction(instruction: string): Promise<void>;

export const SEND_INSTRUCTION_EX_PY = @agent.on_task_complete("collect_order_id") def on_order_id_collected(call: guava.Call): order = lookup_order(call.get_field("order_id")) call.send_instruction( f"Order #{order['id']} is {order['status']} " f"with an estimated delivery of {order['eta']}. " f"Share this with the caller naturally." );

export const SEND_INSTRUCTION_EX_TS = agent.onTaskComplete("collect_order_id", async (call: guava.Call) => { const order = await lookupOrder(await call.getField("order_id") as string); await call.sendInstruction( \Order #${order.id} is ${order.status} ` + `with an estimated delivery of ${order.eta}. ` + `Share this with the caller naturally.` ); })`;

send_instruction()

call.send_instruction(instruction) sends a real-time instruction to the agent without changing the current task. Use it for context injection and behavioral nudges.

Signature

<CodeTabs python={{ code: SEND_INSTRUCTION_SIG_PY, filename: "signature" }} typescript={{ code: SEND_INSTRUCTION_SIG_TS, filename: "signature" }} />

<PropTable rows={[ { name: "instruction", type: "str", desc: "A real-time instruction to pass to the agent. Does not change the current task.", }, ]} />

Return value: None / Promise<void>

Example

<CodeTabs python={{ code: SEND_INSTRUCTION_EX_PY, filename: "example.py" }} typescript={{ code: SEND_INSTRUCTION_EX_TS, filename: "example.ts" }} />

Tip: Unlike `call.set_task()`, `call.send_instruction()` doesn't replace the agent's current objective. Use it to inject context or steer behavior mid-conversation — for example, after a database lookup reveals something the agent should know.

import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink } from '../views/docs/prose';

export const SIG_PY = call.set_variable(key: str, value: Any) -> None call.get_variable(key: str) -> Any;

export const SIG_TS = await call.setVariable(key: string, value: any): Promise<void> await call.getVariable(key: string): Promise<any>;

export const SET_GET_VARIABLE_EX_PY = `@agent.on_call_start def on_call_start(call: guava.Call): # Variables seeded via call_phone(variables={...}) are readable immediately patient_name = call.get_variable("patient_name") call.reach_person(contact_full_name=patient_name)

@agent.on_reach_person def on_reach_person(call: guava.Call, outcome: str): if outcome == "available": call.set_task( objective="Confirm the appointment and answer any questions.", on_complete=on_confirmed, )

@agent.on_task_complete("confirmed") def on_confirmed(call: guava.Call): call.hangup()`;

export const SET_GET_VARIABLE_EX_TS = `agent.onCallStart(async (call: guava.Call) => { // Variables seeded via callPhone({ variables: {...} }) are readable immediately const patientName = await call.getVariable("patientName"); await call.reachPerson(patientName); });

agent.onReachPerson(async (call: guava.Call, outcome: string) => { if (outcome === "available") { await call.setTask({ objective: "Confirm the appointment and answer any questions.", }); } });

agent.onTaskComplete("confirmed", async (call: guava.Call) => { await call.hangup(); });`;

set_variable() / get_variable()

Call variables are provided as a convenient way to pass per-call data (patient name, account ID, etc.) between agent handlers. Variables can be seeded when the call starts and read or updated at any point during the call.

<CodeTabs python={{ code: SIG_PY, filename: "signature" }} typescript={{ code: SIG_TS, filename: "signature" }} />

Valid variable values

Variable values must be JSON-serializable: strings, numbers, booleans, None, and dicts/lists composed of those types.

Seeding variables at call start

For the following types of calls, variables can be seeded at the start.

  • Outbound calls — pass a variables dict to call_phone() / callPhone()
  • Campaigns — each contact's data dict becomes that contact's variables

Other ways to store call state

As an alternative to call variables, you can keep per-call state in an in-process dictionary keyed by call.id.

That said, we only recommend this for simple use cases. If your process restarts, any in-memory state will be lost. For durable per-call state, use Redis or another session store keyed by call.id.

r = redis.Redis()
call_state: dict[str, dict] = {}

@agent.on_call_start
def on_call_start(call: guava.Call):
    # In-memory — lost on process restart
    call_state[call.id] = {"stage": "intro"}

# Redis — survives restarts
    r.set(f"call_state:{call.id}", json.dumps({"stage": "intro"}), ex=3600)

Example

<CodeTabs python={{ code: SET_GET_VARIABLE_EX_PY, filename: "example.py" }} typescript={{ code: SET_GET_VARIABLE_EX_TS, filename: "example.ts" }} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink, PropTable } from '../views/docs/prose'; export const SET_LANGUAGE_MODE_SIG_PY = call.set_language_mode( primary: Language = "english", secondary: list[Language] | None = None, );

export const SET_LANGUAGE_MODE_SIG_TS = `await call.setLanguageMode({ primary?: Language, // default: "english" secondary?: Language[], })

// Language = "english" | "spanish" | "french" | "german" | "italian"`;

export const SET_LANGUAGE_MODE_EX1_PY = @agent.on_call_start def on_call_start(call: guava.Call): call.set_persona(organization_name="Harper Valley Property Insurance") call.set_language_mode(primary="english", secondary=["spanish"]) call.set_task( task_id="intro", objective="Answer questions regarding property insurance policy.", );

export const SET_LANGUAGE_MODE_EX1_TS = agent.onCallStart(async (call: guava.Call) => { await call.setPersona({ organizationName: "Harper Valley Property Insurance" }); await call.setLanguageMode({ primary: "english", secondary: ["spanish"] }); await call.setTask({ taskId: "intro", objective: "Answer questions regarding property insurance policy.", }); });;

export const SET_LANGUAGE_MODE_EX2_PY = call.set_language_mode( primary="english", secondary=["spanish", "french", "german"], );

export const SET_LANGUAGE_MODE_EX2_TS = await call.setLanguageMode({ primary: "english", secondary: ["spanish", "french", "german"], });;

set_language_mode()

Configures the voice agent to understand and respond in additional languages beyond English. The agent starts in the primary language and switches to a secondary language when the caller requests it or speaks in that language.

<CodeTabs python={{ code: SET_LANGUAGE_MODE_SIG_PY, filename: "signature" }} typescript={{ code: SET_LANGUAGE_MODE_SIG_TS, filename: "signature" }} />

<PropTable rows={[ { name: "primary", type: "Language", default: '"english"', desc: "The language the agent starts the conversation in.", }, { name: "secondary", type: "list[Language] | None", default: "None", desc: "Additional languages the agent can switch to when the caller requests them.", }, ]} />

The Language type is defined as:

Literal["english", "spanish", "french", "german", "italian"]

Return value: None / Promise<void>

Example: single secondary language

<CodeTabs python={{ code: SET_LANGUAGE_MODE_EX1_PY, filename: "example.py" }} typescript={{ code: SET_LANGUAGE_MODE_EX1_TS, filename: "example.ts" }} />

Example: multiple secondary languages

<CodeTabs python={{ code: SET_LANGUAGE_MODE_EX2_PY, filename: "example.py" }} typescript={{ code: SET_LANGUAGE_MODE_EX2_TS, filename: "example.ts" }} />

Transcript behavior

  • There is no auto-translation on the transcript. Each turn appears in the language it was spoken in — if the caller speaks Spanish, that turn is in Spanish; if they speak English, that turn is in English.
  • There is currently no per-turn language marker in the transcript.

Edge cases

  • If secondary is None or empty, the agent operates in primary only (the current default behavior).
  • When a non-English language is detected during a call, the system automatically switches to a language-specific TTS voice.
  • The grace voice has clones for Spanish, French, German, and Italian. If no dedicated clone exists for a voice + language combination, the base voice is used.
Compliance: English and Spanish are currently supported for HITRUST / PCI-compliant deployments. All languages other than English and Spanish are not. Support for other languages is planned.

import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink, PropTable } from '../views/docs/prose'; export const TRANSFER_SIG_PY = call.transfer( destination: str, instructions: str | None = None, );

export const TRANSFER_SIG_TS = await call.transfer( destination: string, instructions?: string, ): Promise<void>;

export const TRANSFER_EX_PY = @agent.on_task_complete("collect_issue") def on_issue_collected(call: guava.Call): call.transfer( destination="+18005550199", instructions="Let the caller know you're transferring them to a service representative.", );

export const TRANSFER_EX_TS = agent.onTaskComplete("collect_issue", async (call: guava.Call) => { await call.transfer( "+18005550199", "Let the caller know you're transferring them to a service representative.", ); });;

transfer()

call.transfer() hands the active call off to another phone number or SIP address. It is a soft transfer — the agent notifies the caller before bridging, so there's no abrupt silence or dead air.

<CodeTabs python={{ code: TRANSFER_SIG_PY, filename: "signature" }} typescript={{ code: TRANSFER_SIG_TS, filename: "signature" }} />

<PropTable rows={[ { name: "destination", type: "str", desc: "The phone number or SIP address to transfer the call to.", }, { name: "instructions", type: "str | None", desc: 'What the agent should say before bridging. Defaults to a generic "I'll transfer you now" message.', }, ]} />

Example

<CodeTabs python={{ code: TRANSFER_EX_PY, filename: "example.py" }} typescript={{ code: TRANSFER_EX_TS, filename: "example.ts" }} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink, PropTable } from '../views/docs/prose'; export const HANGUP_SIG_PY = call.hangup(final_instructions: str = "");

export const HANGUP_SIG_TS = await call.hangup(final_instructions?: string): Promise<void>;

export const HANGUP_EX_PY = @agent.on_task_complete("collect_order") def on_order_collected(call: guava.Call): call.hangup( final_instructions="Thank them for their time, mention the confirmation number, then hang up." );

export const HANGUP_EX_TS = agent.onTaskComplete("collect_order", async (call: guava.Call) => { await call.hangup( "Thank them for their time, mention the confirmation number, then hang up." ); });;

hangup()

call.hangup() is a soft hangup. Rather than cutting the call immediately, it hands the agent a final instruction and lets it close the conversation naturally before ending the call. Callers hear a proper goodbye.

<CodeTabs python={{ code: HANGUP_SIG_PY, filename: "signature" }} typescript={{ code: HANGUP_SIG_TS, filename: "signature" }} />

<PropTable rows={[ { name: "final_instructions", type: "str", desc: "What the agent should do before hanging up. If omitted, the agent ends the conversation naturally with no special instructions.", }, ]} />

Example

<CodeTabs python={{ code: HANGUP_EX_PY, filename: "example.py" }} typescript={{ code: HANGUP_EX_TS, filename: "example.ts" }} />

Tip: Be specific in your final instructions. The agent will try to fulfill them naturally — including mentioning a confirmation number, scheduling next steps, or expressing appropriate warmth.

import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink, PropTable } from '../views/docs/prose'; export const REACH_PERSON_SIG_PY = def reach_person( contact_full_name: str, *, greeting: str | None = None, voicemail_message: str | None = None, voicemail_hangup: bool = False, outcomes: list[ReachPersonOutcome] | None = None, );

export const REACH_PERSON_SIG_TS = reachPerson( contactFullName: string, options?: { greeting?: string; voicemailMessage?: string; voicemailHangup?: boolean; outcomes?: ReachPersonOutcome[]; }, ): Promise<void>;

export const REACH_PERSON_EX_PY = `@agent.on_call_start def on_call_start(call: guava.Call): call.reach_person( contact_full_name=call.get_variable("contact_name"), voicemail_message="Please give us a call back at your convenience." )

@agent.on_reach_person def on_reach_person(call: guava.Call, outcome: str): if outcome == "available": call.set_task( "main_task", checklist=[...], ) else: call.hangup("Appropriately end the call.")`;

export const REACH_PERSON_EX_TS = `agent.onCallStart(async (call: guava.Call) => { await call.reachPerson(await call.getVariable("contactName") as string, { voicemailMessage: "Please give us a call back at your convenience.", }); });

agent.onReachPerson(async (call: guava.Call, outcome: string) => { if (outcome === "available") { await call.setTask({ taskId: "main_task", checklist: [...] }); } else { await call.hangup("Appropriately end the call."); } })`;

reach_person()

For outbound calls, reach_person() handles the critical first step: confirming you have the right person on the line before proceeding. It automatically handles answering machines, gatekeepers, wrong numbers, and refusals.

<CodeTabs python={{ code: REACH_PERSON_SIG_PY, filename: "signature" }} typescript={{ code: REACH_PERSON_SIG_TS, filename: "signature" }} />

<PropTable rows={[ { name: "contact_full_name", type: "str", desc: "The full name of the person you're trying to reach.", }, { name: "greeting", type: "str | None", default: "None", desc: "Custom greeting message. Overrides the default introduction.", }, { name: "voicemail_message", type: "str | None", default: "None", desc: "Message to leave if voicemail is reached. Mutually exclusive with voicemail_hangup and with set_voicemail_action().", }, { name: "voicemail_hangup", type: "bool", default: "False", desc: "Immediately hang up if voicemail is reached. Mutually exclusive with voicemail_message and with set_voicemail_action().", }, { name: "outcomes", type: "list[ReachPersonOutcome] | None", default: "None", desc: "Custom outcome routing. Defaults to five outcomes: available, unavailable, voicemail, wrong_number, and do_not_contact. Use this to define additional or different outcomes.", }, ]} />

<CodeTabs python={{ code: REACH_PERSON_EX_PY, filename: "example.py" }} typescript={{ code: REACH_PERSON_EX_TS, filename: "example.ts" }} />

What happens on the call

When reach_person() is invoked, the agent automatically:

  1. Greets whoever answers and introduces itself (organization + purpose).
  2. Asks for the contact by name. If someone else answered, asks to speak with or be transferred to the contact.
  3. Determines availability and records the contact's availability in a contact_availability field.
  4. Fires agent.on_reach_person with the outcome key. The five default outcomes are:
    • "available" — the intended contact is on the line.
    • "unavailable" — someone else or an IVR answered and the contact could not be reached.
    • "voicemail" — an answering machine or voicemail system was reached.
    • "wrong_number" — the number does not belong to the contact.
    • "do_not_contact" — the contact asked not to be called again.

If you provided custom outcomes, those are used instead.

Voicemail handling

Warning: reach_person() and set_voicemail_action() both handle voicemail and cannot be used together. If you set voicemail_message or voicemail_hangup on reach_person(), do not call set_voicemail_action() — and vice versa. Using both raises an error.

Common mistake: redundant introductions

Warning: By the time `on_reach_person` fires, the agent has already introduced itself and stated the purpose of the call. Do **not** re-introduce in the first task after `reach_person`.
# WRONG — redundant introduction
@agent.on_reach_person
def on_reach_person(call: guava.Call, outcome: str):
    if outcome == "available":
        call.set_task("survey", checklist=[
            guava.Say("Hi, this is Grace from Acme Corp, I'm calling about..."),  # Already said this
            ...
        ])

# RIGHT — go straight to content
@agent.on_reach_person
def on_reach_person(call: guava.Call, outcome: str):
    if outcome == "available":
        call.set_task("survey", checklist=[
            guava.Say("I just have a few quick questions for you today."),
            ...
        ])

import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink } from '../views/docs/prose';

export const SET_VOICEMAIL_ACTION_SIG_PY = call.set_voicemail_action( hangup: bool = False, message: str | None = None, );

export const SET_VOICEMAIL_ACTION_SIG_TS = await call.setVoicemailAction( action: { hangup: true } | { message: string }, ): Promise<void>;

export const SET_VOICEMAIL_ACTION_EX_PY = @agent.on_call_start def on_call_start(call: guava.Call): call.set_voicemail_action( message="Hi, this is Alex from Bright Smile Dental. Please call us back at 555-0100 to confirm your appointment.", );

export const SET_VOICEMAIL_ACTION_EX_TS = agent.onCallStart(async (call: guava.Call) => { await call.setVoicemailAction({ message: "Hi, this is Alex from Bright Smile Dental. Please call us back at 555-0100 to confirm your appointment.", }); });;

set_voicemail_action()

set_voicemail_action() tells the agent what to do if it reaches an answering machine.

Warning: set_voicemail_action() and reach_person() both handle voicemail and cannot be used together. If you are using reach_person(), set voicemail behavior there via its voicemail_message or voicemail_hangup parameters instead. Using both raises an error.

<CodeTabs python={{ code: SET_VOICEMAIL_ACTION_SIG_PY, filename: "signature" }} typescript={{ code: SET_VOICEMAIL_ACTION_SIG_TS, filename: "signature" }} />

Example

<CodeTabs python={{ code: SET_VOICEMAIL_ACTION_EX_PY, filename: "example.py" }} typescript={{ code: SET_VOICEMAIL_ACTION_EX_TS, filename: "example.ts" }} />

Tip: You must specify exactly one of hangup or message — passing both or neither raises an error.

import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink } from '../views/docs/prose'; export const READ_SCRIPT_SIG_PY = def read_script(script: str);

export const READ_SCRIPT_SIG_TS = readScript(script: string): Promise<void>;

export const READ_SCRIPT_EX_PY = `@agent.on_call_start def on_call_start(call: guava.Call): call.read_script( "Hello! This is a courtesy call from Bright Smile Dental. " "We're confirming your appointment tomorrow at 2 PM." ) call.set_task( "confirm_appointment", checklist=[ guava.Field(key="confirmed", field_type="text", description="Did they confirm the appointment?"), ], )

@agent.on_task_complete("confirm_appointment") def on_confirmed(call: guava.Call): call.hangup()`;

export const READ_SCRIPT_EX_TS = `agent.onCallStart(async (call: guava.Call) => { await call.readScript( "Hello! This is a courtesy call from Bright Smile Dental. " + "We're confirming your appointment tomorrow at 2 PM." ); await call.setTask({ taskId: "confirm_appointment", checklist: [ guava.Field({ key: "confirmed", fieldType: "text", description: "Did they confirm the appointment?", }), ], }); });

agent.onTaskComplete("confirm_appointment", async (call) => { await call.hangup(); })`;

read_script()

read_script() speaks a verbatim opening statement at the very start of a call, before any LLM involvement. Use it for compliance disclosures, scripted greetings, or anything that must be delivered word-for-word.

<CodeTabs python={{ code: READ_SCRIPT_SIG_PY, filename: "signature" }} typescript={{ code: READ_SCRIPT_SIG_TS, filename: "signature" }} />

<CodeTabs python={{ code: READ_SCRIPT_EX_PY, filename: "example.py" }} typescript={{ code: READ_SCRIPT_EX_TS, filename: "example.ts" }} />

Note: Unlike `Say` in a checklist, `read_script()` fires before any LLM turn and before any task is set. It's the agent's very first words.

import { CodeTabs } from '../views/docs/CodeTabs'; import { NextLink, PropTable } from '../views/docs/prose';

export const ADD_INFO_SIG_PY = def add_info(label: str, info: Any) -> None;

export const ADD_INFO_SIG_TS = addInfo(label: string, info: any): Promise<void>;

export const ADD_INFO_EX_PY = `AMENITIES_INFO = { "amenities": [ "Rooftop pool", "Full-service spa", "Fitness center", "Business center", "Complimentary airport shuttle", ] }

agent = guava.Agent( name="Riley", organization="Oceanfront Hotel", purpose="You are the head concierge tasked with assisting guests with questions and reservations.", )

@agent.on_call_start def on_call_start(call: guava.Call): call.add_info("amenities_details", AMENITIES_INFO)`;

export const ADD_INFO_EX_TS = `const AMENITIES_INFO = { amenities: [ "Rooftop pool", "Full-service spa", "Fitness center", "Business center", "Complimentary airport shuttle", ], };

const agent = new guava.Agent({ name: "Riley", organization: "Oceanfront Hotel", purpose: "You are the head concierge tasked with assisting guests with questions and reservations.", });

agent.onCallStart(async (call: guava.Call) => { await call.addInfo("amenities_details", AMENITIES_INFO); })`;

add_info()

add_info() can be used to provide Guava agents with additional context. Once called, the information persists for the duration of the call and surfaces naturally when relevant. It can be called at the start of a call as well as any time during a call.

<CodeTabs python={{ code: ADD_INFO_SIG_PY, filename: "signature" }} typescript={{ code: ADD_INFO_SIG_TS, filename: "signature" }} />

Example

<CodeTabs python={{ code: ADD_INFO_EX_PY, filename: "example.py" }} typescript={{ code: ADD_INFO_EX_TS, filename: "example.ts" }} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink } from '../views/docs/prose'; export const GET_FIELD_SIG_PY = def get_field(field_key: str) -> str | int | dict | None;

export const GET_FIELD_SIG_TS = getField(key: string): Promise<any | null>;

export const GET_FIELD_EX_PY = @agent.on_task_complete("schedule_appointment") def on_appointment_scheduled(call: guava.Call): appointment_time = call.get_field("appointment_time") patient_name = call.get_field("patient_name") # Write to your CRM / EHR save_appointment(patient_name, appointment_time) call.hangup();

export const GET_FIELD_EX_TS = agent.onTaskComplete("schedule_appointment", async (call: guava.Call) => { const appointmentTime = await call.getField("appointment_time"); const patientName = await call.getField("patient_name"); // Write to your CRM / EHR saveAppointment(patientName, appointmentTime); await call.hangup(); });

get_field()

After the checklist completes and agent.on_task_complete fires, use call.get_field() to retrieve collected values by their key. This is where you write results to your CRM, database, or EHR.

<CodeTabs python={{ code: GET_FIELD_SIG_PY, filename: "signature" }} typescript={{ code: GET_FIELD_SIG_TS, filename: "signature" }} />

<CodeTabs python={{ code: GET_FIELD_EX_PY, filename: "example.py" }} typescript={{ code: GET_FIELD_EX_TS, filename: "example.ts" }} />

Return types by field type

The type of the value returned by get_field() depends on the field's field_type:

field_type Returned value
text str
date dict with keys year, month, day (all int)
integer int
multiple_choice str (guaranteed to be one of the values in choices or returned by choice_generator)
calendar_slot ISO-8601 datetime string (e.g. "2022-12-25T16:30")
Tip: You can call `get_field()` at any point after the field has been collected — not just in `on_task_complete`. Use it in mid-call callbacks to personalize subsequent steps.

import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink } from '../views/docs/prose';

export const NEW_INTENT_RECOGNIZER_SIG_PY = `from guava.helpers.llm import IntentRecognizer

IntentRecognizer(intent_choices: list[str] | dict[str, str]) recognizer.classify(intent: str) -> list[SuggestedAction] | None`;

export const NEW_INTENT_RECOGNIZER_SIG_TS = // IntentRecognizer from guava.helpers.llm is not yet available in TypeScript.;

export const NEW_INTENT_RECOGNIZER_EX_PY = `import guava from guava import Agent, SuggestedAction from guava.helpers.llm import IntentRecognizer

agent = Agent( name="Support", organization="Acme Corp", purpose="Help the caller with their support request.", )

intent_recognizer = IntentRecognizer({ 'check order status': 'Caller wants to look up the status of an existing order.', 'bill pay': 'Caller wants to make a payment or ask about their bill.', 'anything else': 'Caller has a request that does not fit the above categories.', })

@agent.on_action_request def on_action_request(call: guava.Call, request: str) -> SuggestedAction | list[SuggestedAction] | None: return intent_recognizer.classify(request)

@agent.on_action("check order status") def check_order_status(call: guava.Call): call.transfer("+15555555555", "Transfer the caller to the order status team.")

@agent.on_action("bill pay") def bill_pay(call: guava.Call): call.transfer("+15555555555", "Transfer the caller to billing.")

@agent.on_action("anything else") def anything_else(call: guava.Call): call.transfer("+15555555555", "Connect the caller with a live agent.")`;

export const NEW_INTENT_RECOGNIZER_EX_TS = // IntentRecognizer from guava.helpers.llm is not yet available in TypeScript.;

export const INTENT_RECOGNIZER_SIG_PY = `from guava.helpers.openai import IntentRecognizer

IntentRecognizer(intent_choices: list[str] | dict[str, str], client: openai.OpenAI | None = None) recognizer.classify(intent: str) -> str | None`;

export const INTENT_RECOGNIZER_SIG_TS = `import { IntentRecognizer } from "@guava-ai/guava-sdk/helpers/openai";

const recognizer = new IntentRecognizer(choices, logger);`;

export const INTENT_RECOGNIZER_EX_PY = `import guava from guava import Agent, SuggestedAction from guava.helpers.openai import IntentRecognizer

agent = Agent( name="Support", organization="Acme Corp", purpose="Help the caller with their support request.", )

intent_recognizer = IntentRecognizer( ['check order status', 'bill pay', 'anything else'] )

@agent.on_action_request def on_action_request(call: guava.Call, request: str) -> SuggestedAction: return SuggestedAction(key=intent_recognizer.classify(request))

@agent.on_action("check order status") def check_order_status(call: guava.Call): call.transfer("+15555555555", "Transfer the caller to the order status team.")

@agent.on_action("bill pay") def bill_pay(call: guava.Call): call.transfer("+15555555555", "Transfer the caller to billing.")

@agent.on_action("anything else") def anything_else(call: guava.Call): call.transfer("+15555555555", "Connect the caller with a live agent.")`;

export const INTENT_RECOGNIZER_EX_TS = `import * as guava from "@guava-ai/guava-sdk"; import { IntentRecognizer } from "@guava-ai/guava-sdk/helpers/openai"; import { getDefaultLogger } from "@guava-ai/guava-sdk";

const agent = new guava.Agent({ name: "Support", organization: "Acme Corp", purpose: "Help the caller with their support request.", });

const choices = ["check_order_status", "bill_pay", "other"] as const; const recognizer = new IntentRecognizer(choices, getDefaultLogger());

agent.onActionRequest(async (_call: guava.Call, request: string) => { const key = await recognizer.classify(request); return { key }; });

agent.onAction("check_order_status", async (call: guava.Call) => { call.transfer("+15555555555", "Transfer the caller to the order status team."); });

agent.onAction("bill_pay", async (call: guava.Call) => { call.transfer("+15555555555", "Transfer the caller to billing."); });

agent.onAction("other", async (call: guava.Call) => { call.transfer("+15555555555", "Connect the caller with a live agent."); })`;

export const INTENT_CLARIFIER_SIG_PY = `from guava.helpers.openai import IntentClarifier

IntentClarifier(intent_choices: list[str] | dict[str, str], client: openai.OpenAI | None = None) clarifier.propose_choices(intent: str) -> list[str]`;

export const INTENT_CLARIFIER_SIG_TS = // IntentClarifier is not yet available in TypeScript. // Use IntentRecognizer for single-match classification.;

export const INTENT_CLARIFIER_EX_PY = `import guava from guava import Agent, SuggestedAction from guava.helpers.openai import IntentClarifier

agent = Agent( name="Scheduler", organization="Acme Corp", purpose="Help callers manage their appointments.", )

intent_clarifier = IntentClarifier( ['reschedule appointment', 'cancel appointment', 'check appointment time'] )

@agent.on_action_request def on_action_request(call: guava.Call, request: str) -> SuggestedAction: matches = intent_clarifier.propose_choices(request) if len(matches) == 1: # Unambiguous — proceed directly return SuggestedAction(key=matches[0]) elif len(matches) > 1: # Ambiguous — route to the most likely match; agent will confirm with caller return SuggestedAction(key=matches[0], description=f"Caller may have meant one of: {matches}") # len == 0: no match, return nothing so the agent keeps listening`;

export const INTENT_CLARIFIER_EX_TS = // IntentClarifier is not yet available in TypeScript.;

Intent Helpers

Guava provides an intent classification helper for routing caller requests. IntentRecognizer classifies caller utterances into your predefined intents and returns matching actions for the dialog engine to handle.

IntentRecognizer

IntentRecognizer classifies a free-text caller utterance against your predefined intent labels and returns all plausible matches as SuggestedAction objects. Use it inside on_action_request() to map caller language to routing decisions — return the full list and let the dialog engine handle disambiguation automatically.

Import: from guava.helpers.llm import IntentRecognizer

<CodeTabs python={{ code: NEW_INTENT_RECOGNIZER_SIG_PY, filename: "signature" }} typescript={{ code: NEW_INTENT_RECOGNIZER_SIG_TS, filename: "signature" }} />

Parameter Type Required Description
intent_choices list[str] | dict[str, str] Yes The set of intents to classify into. Pass a list of choice strings, or a dict mapping choice strings to plain-English descriptions to help IntentRecognizer disambiguate meaning. When a dict is passed, descriptions are also attached to the returned SuggestedAction objects so the dialog engine can use them when disambiguating multiple matches with the caller.

classify(intent: str) -> list[SuggestedAction] | None — Returns all choices from intent_choices that plausibly match intent, ordered by likelihood. Returns None if no choice matches. It is recommended to return the full list from on_action_request to let the dialog engine handle disambiguation automatically.

<CodeTabs python={{ code: NEW_INTENT_RECOGNIZER_EX_PY, filename: "support_agent.py" }} typescript={{ code: NEW_INTENT_RECOGNIZER_EX_TS, filename: "support_agent.ts" }} />


IntentRecognizer (openai — deprecated)

Deprecated: `IntentRecognizer` from `guava.helpers.openai` is deprecated. Use the new `IntentRecognizer` from `guava.helpers.llm` above instead.

IntentRecognizer classifies a free-text caller utterance into one of your predefined intent labels. Use it inside on_action_request() to map vague caller language to clean routing decisions.

<CodeTabs python={{ code: INTENT_RECOGNIZER_SIG_PY, filename: "signature" }} typescript={{ code: INTENT_RECOGNIZER_SIG_TS, filename: "signature" }} />

Parameter Type Required Description
intent_choices list[str] | dict[str, str] Yes The set of intents to classify into. Pass a list of choice strings, or a dict mapping choice strings to plain-English descriptions (descriptions improve accuracy on similar-sounding choices).
client openai.OpenAI No An OpenAI client to use. If omitted, a client is created automatically.

classify(intent: str) -> str | None — Returns the single choice string from intent_choices that best matches intent, or None if the model cannot match any choice. When intent_choices is a dict, the keys are the valid return values; values are used only as descriptions to guide the model.

<CodeTabs python={{ code: INTENT_RECOGNIZER_EX_PY, filename: "support_agent.py" }} typescript={{ code: INTENT_RECOGNIZER_EX_TS, filename: "support_agent.ts" }} />

IntentClarifier (deprecated)

Deprecated: `IntentClarifier` from `guava.helpers.openai` is deprecated. Use the new `IntentRecognizer` from `guava.helpers.llm` above instead — it returns all plausible matches by default, replacing the need for a separate clarifier.

IntentClarifier analyzes a caller's intent and returns the subset of choices that could plausibly match, ordered by likelihood. Use this when an intent may be ambiguous and you need to surface options for the caller to confirm.

<CodeTabs python={{ code: INTENT_CLARIFIER_SIG_PY, filename: "signature" }} typescript={{ code: INTENT_CLARIFIER_SIG_TS, filename: "signature" }} />

Parameter Type Required Description
intent_choices list[str] | dict[str, str] Yes The set of intents to match against. Same format as IntentRecognizer.
client openai.OpenAI No An OpenAI client to use. If omitted, a client is created automatically.

propose_choices(intent: str) -> list[str] — Returns a list of choices that could match intent, ordered by likelihood:

  • One element if the intent clearly maps to a single choice.
  • Multiple elements if the intent is ambiguous.
  • Empty list if the intent matches none of the provided choices.

An empty list means the caller's intent is out-of-scope — not that an error occurred. When intent_choices is a dict, only the keys appear in the returned list.

<CodeTabs python={{ code: INTENT_CLARIFIER_EX_PY, filename: "scheduler_agent.py" }} typescript={{ code: INTENT_CLARIFIER_EX_TS, filename: "scheduler_agent.ts" }} />


import { CodeTabs } from '../views/docs/CodeTabs'; import { CodeBlock } from '../views/docs/CodeBlock'; import { Callout, NextLink } from '../views/docs/prose'; export const DOCUMENT_QA_SIG_PY = `from guava.helpers.rag import DocumentQA

DocumentQA( store=None, # VectorStore for local mode; omit for server mode documents=None, # str or list[str] — documents to index ids=None, # list[str] — stable IDs for upsert/delete chunk_size=5000, # max chars per chunk (local mode only) chunk_overlap=200, # overlap between chunks (local mode only) instructions=None, # system instruction override *, generation_model=None, # GenerationModel (required for local mode) namespace=None, # server-mode namespace for concurrent instances )`;

export const DOCUMENT_QA_SIG_TS = // The new guava.helpers.rag.DocumentQA is not yet available in TypeScript. // For TypeScript projects, see the DocumentQA (Legacy) page.;

export const DOCUMENT_QA_EX_PY = `from guava.helpers.rag import DocumentQA

Server mode (default) — documents stored and queried on Guava's server

qa = DocumentQA(documents=[policy_text, faq_text], namespace="policy_faq") answer = qa.ask("What is the deductible?")

Server mode — multiple concurrent instances (use namespace to isolate)

dental = DocumentQA(documents=dental_docs, namespace="dental") restaurant = DocumentQA(documents=restaurant_docs, namespace="restaurant") dental.ask("What is the copay?") # only searches dental docs restaurant.ask("Do you have vegan options?") # only searches restaurant docs

Local mode — Gemini (guava-sdk[genai])

from google import genai from guava.helpers.lancedb import LanceDBStore from guava.helpers.genai import GenAIEmbedding, GenAIGeneration

client = genai.Client(vertexai=True, project="my-project", location="us-central1") store = LanceDBStore("gs://my-bucket/lancedb", embedding_model=GenAIEmbedding(client=client)) qa = DocumentQA(store=store, generation_model=GenAIGeneration(client=client)) qa.upsert_document("policy", my_text) answer = qa.ask("What is the deductible?")

Local mode — OpenAI (guava-sdk[openai])

import openai from guava.helpers.lancedb import LanceDBStore from guava.helpers.openai import OpenAIEmbedding, OpenAIGeneration

openai_client = openai.OpenAI() # or AzureOpenAI / custom base_url store = LanceDBStore("./lancedb_data", embedding_model=OpenAIEmbedding(client=openai_client)) qa = DocumentQA(store=store, generation_model=OpenAIGeneration(client=openai_client)) qa.upsert_document("policy", my_text) answer = qa.ask("What is the deductible?")

Wiring into an Agent

import guava from guava import Agent

agent = Agent(name="Support", organization="Acme Corp", purpose="Answer customer questions.") document_qa = DocumentQA(documents=some_text)

@agent.on_question def on_question(call: guava.Call, question: str) -> str: return document_qa.ask(question)`;

export const DOCUMENT_QA_EX_TS = // guava.helpers.rag.DocumentQA is not yet available in TypeScript.;

export const DOCUMENT_QA_MGMT_EX_PY = `from guava.helpers.rag import DocumentQA

Load initial documents with stable IDs

qa = DocumentQA( documents=[policy_v1, faq_v1, terms_v1], ids=["policy", "faq", "terms"], namespace="insurance", )

Later: policy was updated — replace it in-place

qa.upsert_document("policy", policy_v2)

Add a new document without a pre-assigned ID

qa.add_document(new_bulletin_text)

Remove a document that's no longer relevant

qa.delete_document("terms")

Wipe everything and start fresh

qa.clear()`;

DocumentQA

DocumentQA answers caller questions against documents using retrieval-augmented generation (RAG). It operates in one of two modes:

  • Server mode (default): Documents are uploaded to the Guava server and questions are answered server-side. Intended for simple use cases with few documents.
  • Local mode: Bring your own vector store and generation model for full control over the RAG pipeline. Guava provides ready-made backends for ChromaDB, LanceDB, pgvector, and Pinecone.

Constructor

<CodeTabs python={{ code: DOCUMENT_QA_SIG_PY, filename: "signature" }} typescript={{ code: DOCUMENT_QA_SIG_TS, filename: "signature" }} />

Parameter Type Required Default Description
store VectorStore | None No None Vector store for local mode. When omitted, server mode is used automatically.
documents list[str] | str | None No None Documents to index at construction time. Accepts a single string or a list.
ids list[str] | None No None Caller-provided IDs for each document, enabling later upsert_document / delete_document. Length must match documents if provided.
chunk_size int No 5000 Maximum characters per chunk (local mode only).
chunk_overlap int No 200 Overlap between consecutive chunks in characters (local mode only).
instructions str | None No None System instruction for the generation model. Overrides the built-in default.
generation_model GenerationModel | None Local mode None Generation model for producing answers. Required when store is provided.
namespace str | None Server mode None Stable string to scope this instance's documents on the server.
namespace requirement: In server mode, `namespace` is required when running multiple `DocumentQA` instances concurrently — even across different files. Without a namespace, concurrent instances may interfere with each other's document stores.

Methods

ask(question: str, k: int = 5) -> str — Retrieve relevant chunks and generate an answer. In server mode, k is ignored (the server uses full document context).

upsert_document(key: str, text: str) -> None — Add or replace a document by key. Stale chunks from a previously longer document are deleted automatically.

add_document(text: str) -> None — Add a document without specifying a key. In server mode, uses a content-derived key (SHA-256 hash).

delete_document(key: str) -> None — Delete a previously upserted document by key.

clear() -> None — Remove all documents from the store.

Available VectorStore Backends (Local Mode)

Class Import Install Default Embedding
ChromaVectorStore guava.helpers.chromadb pip install 'guava-sdk[chromadb]' Built-in all-MiniLM-L6-v2 (no API needed)
LanceDBStore guava.helpers.lancedb pip install 'guava-sdk[lancedb]' Required — pass an EmbeddingModel (GenAIEmbedding, OpenAIEmbedding, PineconeInferenceEmbedding, or a custom subclass)
PgVectorStore guava.helpers.pgvector pip install 'guava-sdk[pgvector]' Required — pass an EmbeddingModel (GenAIEmbedding, OpenAIEmbedding, PineconeInferenceEmbedding, or a custom subclass)
PineconeVectorStore guava.helpers.pinecone pip install 'guava-sdk[pinecone]' multilingual-e5-large via Pinecone Inference

Embedding and generation provider extras: pip install 'guava-sdk[genai]' (Google Gemini) or pip install 'guava-sdk[openai]' (OpenAI). See the Vector Stores reference for full constructor details and backend-specific options.

Examples

<CodeTabs python={{ code: DOCUMENT_QA_EX_PY, filename: "document_qa_examples.py" }} typescript={{ code: DOCUMENT_QA_EX_TS, filename: "document_qa_examples.ts" }} />

Incremental Document Management

Use ids to assign stable keys to documents at construction time, then use upsert_document, delete_document, and clear to manage documents without re-creating the DocumentQA instance.


import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink } from '../views/docs/prose'; export const DATETIME_FILTER_SIG_PY = `from guava.helpers.openai import DatetimeFilter

DatetimeFilter(source_list: list[str], client: openai.OpenAI | None = None) dt_filter.filter(query: str, max_results: int = 5) -> tuple[list[str], list[str]]`;

export const DATETIME_FILTER_SIG_TS = `import { DatetimeFilter } from "@guava-ai/guava-sdk/helpers/openai";

const dtFilter = new DatetimeFilter({ sourceList: string[] }); dtFilter.filter(query: string, maxResults?: number): Promise<[string[], string[]]>`;

export const DATETIME_FILTER_EX_PY = `from guava.helpers.openai import DatetimeFilter

AVAILABLE_SLOTS = [ "2026-04-16T09:00:00", "2026-04-16T10:30:00", "2026-04-17T14:00:00", "2026-04-18T09:00:00", ]

dt_filter = DatetimeFilter(source_list=AVAILABLE_SLOTS)

matches, suggestions = dt_filter.filter("tomorrow morning", max_results=3)

matches == ["2026-04-16T09:00:00", "2026-04-16T10:30:00"]

suggestions == [] (not needed — matches were found)

matches, suggestions = dt_filter.filter("this Friday at noon", max_results=3)

matches == [] (no Friday noon slot exists)

suggestions == ["2026-04-17T14:00:00", ...] (nearby alternatives offered)`;

export const DATETIME_FILTER_EX_TS = `import { DatetimeFilter } from "@guava-ai/guava-sdk/helpers/openai";

const AVAILABLE_SLOTS = [ "2026-04-16T09:00:00", "2026-04-16T10:30:00", "2026-04-17T14:00:00", "2026-04-18T09:00:00", ];

const dtFilter = new DatetimeFilter({ sourceList: AVAILABLE_SLOTS });

const [matches, suggestions] = await dtFilter.filter("tomorrow morning", 3); // matches == ["2026-04-16T09:00:00", "2026-04-16T10:30:00"] // suggestions == [] (not needed — matches were found)`;

export const DATETIME_FILTER_FIELD_EX_PY = `import guava from guava import Agent from guava.helpers.openai import DatetimeFilter

agent = Agent( name="Scheduler", organization="Acme Corp", purpose="Help callers schedule appointments.", )

datetime_filter = DatetimeFilter(source_list=AVAILABLE_SLOTS)

@agent.on_call_start def on_call_start(call: guava.Call): call.set_task( "schedule_appointment", checklist=[ guava.Field( key="appointment_time", field_type="calendar_slot", description="Find a time that works for the caller", searchable=True, ), ], )

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

export const DATETIME_FILTER_FIELD_EX_TS = `import * as guava from "@guava-ai/guava-sdk"; import { DatetimeFilter } from "@guava-ai/guava-sdk/helpers/openai";

const agent = new guava.Agent({ name: "Scheduler", organization: "Acme Corp", purpose: "Help callers schedule appointments.", });

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

agent.onCallStart(async (call: guava.Call) => { await call.setTask({ taskId: "schedule_appointment", checklist: [ guava.Field({ key: "appointment_time", fieldType: "calendar_slot", description: "Find a time that works for the caller", searchable: true, }), ], }); });

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

DatetimeFilter

DatetimeFilter filters a list of ISO 8601 datetime strings to find entries matching a natural-language query (e.g. "tomorrow afternoon"). Returns both matching datetimes and fallback suggestions when no exact match exists.

Constructor

<CodeTabs python={{ code: DATETIME_FILTER_SIG_PY, filename: "signature" }} typescript={{ code: DATETIME_FILTER_SIG_TS, filename: "signature" }} />

Parameter Type Required Description
source_list list[str] Yes The pool of available appointment datetimes in ISO 8601 format (e.g. "2026-03-02T09:00:00"). The model will only return values present in this list.
client openai.OpenAI No An OpenAI client to use. If omitted, a client is created automatically.

Methods

filter(query: str, max_results: int = 5) -> tuple[list[str], list[str]]

Returns a 2-tuple (matching_appointments, other_appointments):

  • matching_appointments: datetimes from source_list that match query, capped at max_results.
  • other_appointments: alternative datetimes to suggest when matching_appointments is empty, also capped at max_results.

Edge Cases

  • Raises AssertionError if max_results is not an int.
  • Both output lists are guaranteed to contain only values drawn from source_list — the model is explicitly instructed never to hallucinate datetimes.
  • Today's date is injected into the prompt automatically, so relative queries like "tomorrow" and "next week" resolve correctly without any date math on the caller's side.
  • other_appointments may be non-empty even when matching_appointments is empty — use it to offer the caller nearby alternatives.
Model details: `DatetimeFilter` uses `gpt-5-mini` with `reasoning.effort = "medium"`. These settings are not configurable.

Basic Usage

<CodeTabs python={{ code: DATETIME_FILTER_EX_PY, filename: "datetime_filter.py" }} typescript={{ code: DATETIME_FILTER_EX_TS, filename: "datetime_filter.ts" }} />

Using with Field and on_search_query

A common pattern is to pair DatetimeFilter with a Field of type "calendar_slot" with searchable=True, and wire the filter into the on_search_query callback:

<CodeTabs python={{ code: DATETIME_FILTER_FIELD_EX_PY, filename: "scheduling_agent.py" }} typescript={{ code: DATETIME_FILTER_FIELD_EX_TS, filename: "scheduling_agent.ts" }} />


import { CodeBlock } from '../views/docs/CodeBlock'; import { Callout, NextLink } from '../views/docs/prose'; export const VECTOR_STORES_EX_PY = `from guava.helpers.rag import DocumentQA from guava.helpers.genai import GenAIEmbedding, GenAIGeneration from google import genai

client = genai.Client(vertexai=True, project="my-project", location="us-central1") embedding = GenAIEmbedding(client=client) # gemini-embedding-001, 768-dim generation = GenAIGeneration(client=client) # gemini-2.5-flash

ChromaDB — no external embedding API required; persists to disk by default

from guava.helpers.chromadb import ChromaVectorStore

store = ChromaVectorStore() # path="./chroma_data" by default store = ChromaVectorStore(path=None) # in-memory/ephemeral

qa = DocumentQA(store=store, generation_model=generation, documents=[doc1, doc2]) answer = qa.ask("What is the deductible?")

LanceDB — local path or GCS URI; requires an embedding model

from guava.helpers.lancedb import LanceDBStore

store = LanceDBStore("./lancedb_data", embedding_model=embedding) store = LanceDBStore("gs://my-bucket/lancedb", embedding_model=embedding) # GCS

qa = DocumentQA(store=store, generation_model=generation, documents=[doc1, doc2]) answer = qa.ask("What is the deductible?")

pgvector — Postgres connection string; table and indexes created automatically

from guava.helpers.pgvector import PgVectorStore

store = PgVectorStore( db_url="postgresql://user:password@localhost:5432/mydb", embedding_model=embedding, ) qa = DocumentQA(store=store, generation_model=generation, documents=[doc1, doc2]) answer = qa.ask("What is the deductible?")

Pinecone — set PINECONE_API_KEY; index and embeddings are fully managed

from guava.helpers.pinecone import PineconeVectorStore

store = PineconeVectorStore() # index_name="guava-chunks" by default

qa = DocumentQA(store=store, generation_model=generation, documents=[doc1, doc2]) answer = qa.ask("What is the deductible?")`;

Vector Stores

Guava provides four ready-made VectorStore implementations that can be passed directly to DocumentQA as the store argument. Each wraps a popular vector database and handles embedding, indexing, and similarity search.

Python only: Vector store backends are currently available in Python only. TypeScript equivalents are not yet available.

Installation

Install only the backend(s) you need:

  • pip install 'guava-sdk[chromadb]'
  • pip install 'guava-sdk[lancedb]'
  • pip install 'guava-sdk[pgvector]'
  • pip install 'guava-sdk[pinecone]'

Embedding and generation provider extras:

  • pip install 'guava-sdk[genai]' — Google Gemini (backs GenAIEmbedding / GenAIGeneration)
  • pip install 'guava-sdk[openai]' — OpenAI (backs OpenAIEmbedding / OpenAIGeneration)

Importing a backend class without the corresponding extra installed raises ImportError with an install hint.

ChromaVectorStore

from guava.helpers.chromadb import ChromaVectorStore

Parameter Type Required Default Description
path str | None No "./chroma_data" Directory for persistent storage. Pass None for an in-memory ephemeral store.
collection_name str No "chunks" ChromaDB collection name.
embedding_model EmbeddingModel | None No None External embedding model. When omitted, ChromaDB's built-in all-MiniLM-L6-v2 model is used — no external API needed.

LanceDBStore

from guava.helpers.lancedb import LanceDBStore

Parameter Type Required Default Description
path str No "./lancedb_data" Local path or GCS URI (e.g. "gs://bucket/lancedb") for storage.
table_name str No "chunks" LanceDB table name.
embedding_model EmbeddingModel Yes — Embedding model to use. Pass a configured instance such as GenAIEmbedding or OpenAIEmbedding.
Note: LanceDB silently drops tables that predate the current schema version. This triggers a full re-index the next time `DocumentQA` ingests documents.

PgVectorStore

from guava.helpers.pgvector import PgVectorStore

Parameter Type Required Default Description
db_url str Yes — PostgreSQL connection string (e.g. "postgresql://user:pass@host/db").
table_name str No "guava_chunks" Table name for stored chunks.
embedding_model EmbeddingModel Yes — Embedding model to use. Pass a configured instance such as GenAIEmbedding or OpenAIEmbedding.

PgVectorStore creates the vector extension, chunks table, and HNSW cosine index automatically on first connect. If the connecting user lacks CREATE EXTENSION privileges, initialization will fail.

Managed Postgres: Managed services (Cloud SQL, AlloyDB, RDS) are untested but expected to work since the implementation uses standard `psycopg`.

PineconeVectorStore

from guava.helpers.pinecone import PineconeVectorStore

Parameter Type Required Default Description
api_key str | None No env PINECONE_API_KEY Pinecone API key. If omitted, reads from the environment.
index_name str No "guava-chunks" Pinecone index name. Created automatically if it does not exist.
cloud str No "aws" Serverless cloud provider for index creation. Ignored if the index already exists.
region str No "us-east-1" Serverless region for index creation. Ignored if the index already exists.
embedding_model EmbeddingModel | None No PineconeInferenceEmbedding Defaults to multilingual-e5-large (1024-dim) via Pinecone's hosted Inference API.
Cold start: Pinecone index creation can take 30–60 seconds on first use. Subsequent instantiations with the same `index_name` skip creation and connect immediately.

PineconeInferenceEmbedding

from guava.helpers.pinecone import PineconeInferenceEmbedding

Parameter Type Required Default Description
pc Pinecone Yes — A configured Pinecone client instance.
model str No "multilingual-e5-large" Pinecone inference model name.
dimensionality int No 1024 Output vector size.

GenAIEmbedding / GenAIGeneration

from guava.helpers.genai import GenAIEmbedding, GenAIGeneration

Install: pip install 'guava-sdk[genai]'

GenAIEmbedding — EmbeddingModel backed by Google Gemini. Works with either a Vertex AI client (genai.Client(vertexai=True, project=..., location=...)) or an AI Studio client (genai.Client(api_key=...)).

Parameter Type Required Default Description
client google.genai.Client Yes — Configured Gemini client.
model str No "gemini-embedding-001" Gemini embedding model name.
dimensionality int No 768 Output vector size.

Uses different task types under the hood: RETRIEVAL_DOCUMENT for embed_documents and QUESTION_ANSWERING for embed_query, which improves retrieval quality versus a single generic embedding.

GenAIGeneration — GenerationModel backed by Google Gemini.

Parameter Type Required Default Description
client google.genai.Client Yes — Configured Gemini client.
model str No "gemini-2.5-flash" Gemini chat model name.
thinking_budget int | None No 0 Token budget for the model's internal thinking step. Default 0 disables thinking on gemini-2.5-flash for faster responses. Pass None for non-thinking models (e.g. gemini-1.5-flash). Pass a positive integer (e.g. 8192) to enable extended thinking.
thinking_budget compatibility: The default `thinking_budget=0` works with `gemini-2.5-flash`. If you switch to a non-thinking model like `gemini-1.5-flash`, pass `thinking_budget=None` — that model raises an error when it receives a `thinking_config`.

OpenAIEmbedding / OpenAIGeneration

from guava.helpers.openai import OpenAIEmbedding, OpenAIGeneration

Install: pip install 'guava-sdk[openai]'

The caller supplies a configured openai.OpenAI instance. The same wrappers work against Azure OpenAI or any OpenAI-compatible base URL — the client object decides where requests go.

OpenAIEmbedding — EmbeddingModel backed by the OpenAI Embeddings API.

Parameter Type Required Default Description
client openai.OpenAI Yes — Configured OpenAI client.
model str No "text-embedding-3-small" OpenAI embedding model name.
dimensionality int No 1536 Output vector size.

OpenAIGeneration — GenerationModel backed by OpenAI chat.completions.

Parameter Type Required Default Description
client openai.OpenAI Yes — Configured OpenAI client.
model str No "gpt-5-mini" OpenAI chat model name.

The system_instruction argument to generate() is mapped to a {"role": "system", ...} message prepended to the user prompt.

GenerationModel

Any implementation of the guava.helpers.rag.GenerationModel interface works with DocumentQA in local mode. The examples on this page use GenAIGeneration, but OpenAIGeneration (above) or any custom GenerationModel subclass works equally well.

Examples


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

API Reference

The Guava REST API lets you manage calls, agents, and account resources directly over HTTP — no SDK required.

Base URL

All requests are made to:

https://api.goguava.ai/v1

Authentication

Include your API key in the Authorization header on every request:

Authorization: Bearer YOUR_GUAVA_API_KEY

POST /v1/check-sdk-deprecation

Check the deprecation status of a specific SDK version.

Request parameters

Name Type Required Description
sdk_name string Yes "python-sdk" or "typescript-sdk"
sdk_version string Yes The version to check (e.g. "0.5.0")

Response fields

Name Type Description
deprecation_status string "supported" or "deprecated"

Example

<CodeBlock filename="terminal" language="bash" code={curl -X POST https://api.goguava.ai/v1/check-sdk-deprecation \\ -H "Authorization: Bearer $GUAVA_API_KEY" \\ -H "Content-Type: application/json" \\ -d '{"sdk_name": "python-sdk", "sdk_version": "0.5.0"}'} />

Sample response:

<CodeBlock filename="response.json" language="json" code={{ "deprecation_status": "supported" }} />

This endpoint is REST-only — there is no SDK or CLI method for it yet.

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

Conversations

These endpoints let you retrieve, inspect, and delete conversation data for completed calls.

All requests require an `Authorization: Bearer YOUR_GUAVA_API_KEY` header. See the API Overview for details.

List conversations

GET /v1/conversations

List your organization's conversations, newest first. Use the filters below to narrow results — for example, to pull every call to or from a given phone number, with each call's duration, to build a usage or billing report.

Parameters

Name Type Required Description
from_number string No Only return calls placed from this number. E.164 format (e.g. +15551234567).
to_number string No Only return calls placed to this number. E.164 format.
direction string No Only return calls in this direction: "inbound" or "outbound" (from the perspective of your agent). Use "all" or omit for both.
date_from string No Only return calls at or after this time (ISO 8601, e.g. 2026-06-01T00:00:00Z).
date_to string No Only return calls at or before this time (ISO 8601).
campaign_id string No Only return calls placed by this outbound campaign.
limit integer No Maximum number of conversations to return, between 1 and 100. Defaults to 50.
after string No Pagination cursor. Pass the next_cursor from the previous response to fetch the next page.

Response

A JSON object with the following fields:

Field Type Description
conversations array The matching conversations, newest first. Each is the same object returned by Get conversation details.
next_cursor string (nullable) Pass as after to fetch the next page. null when there are no more results.
has_more boolean true if more conversations match than were returned in this response.

Each conversation includes the call's duration_sec, so you can total call time without fetching each conversation individually.

Pagination

Conversations are returned newest first. When has_more is true, request the next page by calling again with after set to the next_cursor from the previous response. Keep paging until has_more is false to retrieve the full set.

Errors

Status Description
400 Invalid limit, after cursor, campaign_id, or date value
401 Invalid authentication
422 from_number or to_number is not a valid E.164 number

Example

curl -G https://api.goguava.ai/v1/conversations \
  -H 'Authorization: Bearer YOUR_GUAVA_API_KEY' \
  --data-urlencode 'from_number=+15551234567' \
  --data-urlencode 'date_from=2026-06-01T00:00:00Z'

Sample response:

{
  "conversations": [
    {
      "id": "6064ab9663dc4eb0",
      "call_id": "6064ab9663dc4eb0",
      "ts": "2026-06-09T00:12:04.518000+00:00",
      "direction": "outbound",
      "from_number": "+15551234567",
      "to_number": "+15551230001",
      "duration_sec": 142,
      "campaign_id": null,
      "termination_reason": "user-hangup"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
The `+` in a phone number must be URL-encoded as `%2B` in query strings. The `curl -G --data-urlencode` form above handles this for you.

Get conversation details

GET /v1/conversations/{call_id}

Retrieve metadata about a single conversation.

Parameters

Name Type Required Description
call_id string Yes ID of the call

Response

A JSON object with the following fields:

Field Type Description
id string Unique ID of the conversation
call_id string ID of the call
ts string Date of the call in ISO 8601 format
direction string "inbound" or "outbound" (from the perspective of your agent)
from_number string Phone number that initiated the call
to_number string Phone number that received the call
duration_sec integer (nullable) How long the call lasted in seconds. null until the call completes.
campaign_id string (nullable) Outbound campaign that initiated this call, if applicable
termination_reason string (nullable) How the call ended (e.g. "user-hangup"), if known

Errors

Status Description
401 Invalid authentication
404 The call with this ID does not exist

Example

curl -H 'Authorization: Bearer YOUR_GUAVA_API_KEY' \
  https://api.goguava.ai/v1/conversations/6064ab9663dc4eb0

Get conversation transcript

GET /v1/conversations/{call_id}/transcript

Download the transcript for a conversation as a list of turns.

Parameters

Name Type Required Description
call_id string Yes ID of the call

Response

A JSON array of turn objects. Each turn has:

Field Type Description
speaker string "HUMAN" or "AGENT"
text string What was said by the speaker
offset_ms integer Milliseconds into the call when this turn began

Errors

Status Description
401 Invalid authentication
404 The call with this ID does not exist

Example

curl -H 'Authorization: Bearer YOUR_GUAVA_API_KEY' \
  https://api.goguava.ai/v1/conversations/6064ab9663dc4eb0/transcript

Get conversation recording

GET /v1/conversations/{call_id}/recording

Download the audio recording for a conversation in WAV format.

Parameters

Name Type Required Description
call_id string Yes ID of the call

Response

WAV audio file.

Errors

Status Description
401 Invalid authentication
404 The call with this ID does not exist

Example

curl -H 'Authorization: Bearer YOUR_GUAVA_API_KEY' \
  -o recording.wav \
  https://api.goguava.ai/v1/conversations/6064ab9663dc4eb0/recording

Delete a conversation

DELETE /v1/conversations/{call_id}

Permanently delete a conversation and its associated data.

Parameters

Name Type Required Description
call_id string Yes ID of the call

Response

None (empty body).

Errors

Status Description
401 Invalid authentication
404 The call with this ID does not exist

Example

curl -X DELETE -H 'Authorization: Bearer YOUR_GUAVA_API_KEY' \
  https://api.goguava.ai/v1/conversations/6064ab9663dc4eb0

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

Messages

These endpoints let you send SMS messages and read the inbound messages received on your Guava numbers — for example, to poll your own inbox for replies.

Send an SMS

POST /v1/send-sms

Send a single SMS from one of your Guava numbers.

Request body

A JSON object with the following fields:

Name Type Required Description
from_number string Yes One of your Guava numbers, in E.164 format. Must have SMS enabled.
to_number string Yes The recipient's number, in E.164 format.
message string Yes The message body to send.

Response

On success, returns 201 Created:

{
  "status": "sent"
}

Errors

Status Description
400 The from_number isn't owned by your organization, or doesn't have SMS configured.
401 Invalid authentication
500 The upstream carrier failed to accept the message.

Example

curl -X POST https://api.goguava.ai/v1/send-sms \
  -H 'Authorization: Bearer YOUR_GUAVA_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"from_number": "+15551230001", "to_number": "+15551234567", "message": "Hello from Guava!"}'

List inbound messages

GET /v1/messages

List the inbound messages received on one of your Guava numbers, oldest first. Poll this endpoint to watch for replies.

Parameters

Name Type Required Description
to_number string Yes The Guava number whose inbox you want to read, in E.164 format. Must be owned by your organization.
start string No Only return messages received at or after this time (ISO 8601, e.g. 2026-06-09T00:00:00Z).
from_number string No Only return messages sent from this number, in E.164 format.
modality string No Only return messages on this channel. Currently "sms".
limit integer No Maximum number of messages to return, between 1 and 100. Defaults to 50.

Response

A JSON object with the following fields:

Field Type Description
messages array The matching messages, oldest first.
has_more boolean true if more messages match than were returned in this response.

Each message has the following fields:

Field Type Description
id string Unique ID of the message
from_number string The number that sent the message
to_number string Your Guava number that received the message
content string The message body
received_at string When the message was received, in ISO 8601 format
modality string The channel the message arrived on. Currently "sms".
direction string Always "inbound"

Pagination

Messages are returned oldest first. When has_more is true, request the next page by calling again with start set to the received_at of the last message you received. Because start is inclusive, that boundary message — and any others sharing its exact timestamp — will be returned again, so dedupe by id.

Errors

Status Description
400 Invalid start timestamp or limit value
401 Invalid authentication
404 The to_number isn't owned by your organization

Example

curl -G https://api.goguava.ai/v1/messages \
  -H 'Authorization: Bearer YOUR_GUAVA_API_KEY' \
  --data-urlencode 'to_number=+15551230001' \
  --data-urlencode 'start=2026-06-09T00:00:00Z'

Sample response:

{
  "messages": [
    {
      "id": "6064ab9663dc4eb0",
      "from_number": "+15551234567",
      "to_number": "+15551230001",
      "content": "YES, see you then!",
      "received_at": "2026-06-09T00:12:04.518000+00:00",
      "modality": "sms",
      "direction": "inbound"
    }
  ],
  "has_more": false
}
The `+` in a phone number must be URL-encoded as `%2B` in query strings. The `curl -G --data-urlencode` form above handles this for you.

import { Callout, NextLink } from '../views/docs/prose';

Outbound Compliance Setup

Before you can make outbound calls or send SMS through Guava, your organization needs to complete a set of carrier and regulatory registrations. This guide walks you through the full process in order.

All registrations are submitted from the Guava Compliance page. Guava handles submission and coordination with carriers on your behalf.

Why this matters

Carriers and regulators require businesses to register before they can send outbound voice or SMS traffic. These registrations serve two purposes: they keep you compliant with federal law, and they directly improve your campaign performance.

Regulatory compliance. The Telephone Consumer Protection Act (TCPA) and FCC regulations govern how businesses can contact consumers by phone and SMS. Non-compliance exposes your organization to significant legal liability — TCPA violations carry statutory damages of $500–$1,500 per call or message. The registrations in this guide establish the documentation trail that demonstrates your organization is operating lawfully.

Better pickup and deliverability rates. Registered businesses are treated differently by carriers. For voice, your phone numbers become associated with a verified business identity, which increases the likelihood that calls are answered rather than blocked or flagged as spam. For SMS, A2P 10DLC registration is required for messages to be delivered at all on major US carriers — unregistered traffic is filtered.

AI voice disclosure. All calls placed through Guava are handled by an AI voice agent. FCC rules require that callers disclose the use of AI-generated voices at the beginning of a call. Guava's agents are configured to make this disclosure automatically, but you are responsible for ensuring your agent scripts and call flows meet the disclosure requirements in every jurisdiction where you operate.

Your compliance responsibilities

Guava handles carrier registration and submission on your behalf, but your organization remains responsible for the following. See our Terms of Service for additional details.

  • Do Not Call (DNC) scrubbing — you must scrub your contact lists against the National DNC Registry and maintain an internal DNC list. Contacts who have opted out must not be dialed.
  • Consent — you are responsible for obtaining and documenting the appropriate level of consent (express written consent for marketing calls/SMS, prior express consent for informational calls) before contacting consumers.
  • Call time restrictions — federal rules under the TCPA restrict outbound calls to between 8 AM and 9 PM in the called party's local time zone. Some states have stricter windows. You are responsible for configuring your campaigns accordingly.
  • AI voice disclosure — you must disclose at the start of each call that the caller is an AI. You are responsible for ensuring your agent's greeting includes this disclosure.
  • State-specific regulations — several states (California, Florida, Texas, and others) have additional restrictions beyond federal law. You are responsible for understanding and complying with the laws in every state where you dial.
  • Opt-out handling — consumers who request to be added to your DNC list must be honored within 30 days and must not be contacted again.

Guava is not a law firm and this is not legal advice. Consult your legal counsel to ensure your outbound program meets all applicable requirements.


Overview

Step Registration Scope Requires
1 Outbound Dialing Registration Once per org —
2 Use Case One per business purpose Step 1 approved
3 SMS Brand Registration Once per org Step 1 approved
4 SMS Campaign Registration One per use case Steps 2 + 3 complete

Steps 2 and 3 can be completed in parallel once Step 1 is approved. Step 4 requires both Steps 2 and 3 to be complete.

Compliant Language Support: English and Spanish are currently supported for HITRUST / PCI-compliant deployments. All languages other than English and Spanish are not. Support for other languages is planned. See: set_language_mode()

Step 1: Outbound Dialing Registration

This is a Know Your Customer (KYC) form that verifies your business identity and establishes your compliance baseline. It is a prerequisite for all other registrations — you cannot proceed until it is approved.

Go to the Guava Compliance page and submit the Outbound Permissions form under General Forms.

What you'll need:

  • Business Identity — legal business name, business type, industry, EIN, physical address, and website
  • Point of Contact — a person authorized to enter agreements on behalf of the company (name, email, phone, title)
  • Compliance Contact — a contact for compliance-related communications (may be the same as above)
  • Call Setup — types of calls planned, use case description, phone number plan, and whether calls involve PHI or cardholder data
  • Telecom Compliance — FCC Robocall Mitigation Database (RMD) registration status, TCPA/TSR certification, consent method, DNC Registry validation, and internal DNC list practices

The submitter must have authority to bind the company to agreements. US-based businesses only; a valid EIN is required.

Once submitted, Guava reviews the form and approves your organization for outbound dialing. You'll receive confirmation when approved.


Step 2: Create a Use Case

A use case is a named grouping for a single business purpose — for example, "Appointment Reminders" or "Collections." Each use case ties together your campaigns, SMS Campaign Registration, and any analytics provider attestation.

Once your Outbound Dialing Registration is approved, go to the Guava Compliance page and click + New Use Case. You'll provide a name and select a type from a standard list of 45 categories (e.g., Appointment Reminders, Lead Generation, Telehealth, Survey/Research).

If you run outbound campaigns for multiple distinct purposes, create a separate use case for each. SMS Campaign Registration is completed once per use case.


Step 3: SMS Brand Registration

SMS Brand Registration registers your organization as an SMS brand with the carrier. This is a one-time, org-wide registration — you only need to complete it once regardless of how many use cases or campaigns you have. It is required before you can send any SMS messages through Guava.

Go to the Guava Compliance page and submit the SMS Brand Registration form under General Forms.

What you'll need:

  • Brand Details — legal company name, DBA/brand name, company type (Public, Private, US Nonprofit, US Government), EIN/Tax ID, and optional alternate identifiers (DUNS, GIIN, LEI)
  • Address — street address, city, state, zip
  • Company Info — website, and stock symbol/exchange if publicly traded
  • Business Contact — name, email, and mobile phone
  • Support Contact — support email and phone number

Once submitted, the registration is forwarded to the carrier. Upon approval, your organization is assigned a business ID and your phone numbers are associated with your brand.


Step 4: SMS Campaign Registration

SMS Campaign Registration (A2P 10DLC) tells the carrier what kind of SMS messages you'll send, how consumers opt in, and what your compliance keyword responses are. You complete this once per use case. Without it, SMS messages for that use case will not be delivered.

Go to the Guava Compliance page, find the relevant use case card, and click SMS Campaign Form.

What you'll need:

  • Campaign Details — campaign type, description of the SMS campaign, how consumers opt in (call-to-action / message flow), CTA URL, terms of service URL, privacy policy URL, and sample messages
  • Compliance Keyword Responses — opt-in keywords and response, opt-out (STOP) response, and help (HELP) response. Each response must include your brand name, campaign description, and support contact info.
  • Usage Disclosures — whether you use number pooling (50+ numbers), direct lending, embedded links (note: public URL shorteners like bit.ly or tinyurl are not permitted), embedded phone numbers, or age-gated content
  • Traffic Profile — SMS traffic type (transactional, customer care, marketing, or mixed), associated website, message frequency, and whether the website/domain is displayed in messages

Once submitted, the registration is forwarded to the carrier for processing. Upon approval, SMS is enabled for that use case and your compliance keyword responses (opt-in, opt-out, help) are configured.

If you have multiple use cases, you'll complete a separate SMS Campaign Registration for each one.

Common mistakes that cause rejections

This is the registration people most frequently get wrong. Carriers review submissions carefully and will reject campaigns that don't meet the requirements below.

Opt-in must be SMS-specific. Your opt-in language must be exclusive to SMS. You cannot combine SMS opt-in with consent for email, phone calls, or other channels in a single checkbox or statement. Each channel must have its own separate opt-in.

Phone number field must be optional. If your opt-in form includes a phone number field, it must not be marked as required. Consumers cannot be required to provide a phone number in order to access a service or complete a transaction.

You can only collect opt-in for yourself. Your opt-in language must refer only to your own organization. You cannot collect opt-in on behalf of marketing partners, agents, affiliates, or third parties. Consumer opt-in is not transferable or sharable — you cannot share opt-in status with any other party, and your Privacy Policy must explicitly state that you do not share SMS opt-in information with third parties.

Opt-out instructions must be included. Every opt-in call-to-action must include opt-out instructions. The standard language is: "Reply STOP to opt-out."

Links to Terms and Privacy Policy are required. Your opt-in call-to-action must include a link to your Terms and Conditions and a link to your Privacy Policy. These must be working, publicly accessible URLs at the time of submission.


import { CodeBlock } from '../views/docs/CodeBlock'; import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, Prose, PropTable, GetStartedCTA } from '../views/docs/prose';

WebRTC Widget

Embed a Guava voice agent on your website so visitors can start a voice conversation directly from the browser. The WebRTC audio widget is a drop-in <script> tag that handles all WebRTC signaling, UI, and state management — no additional CSS, JS, or dependencies required.

Integration

Add a single <script> tag to your page. The only required attribute is data-webrtc-code, which is the agent code (starts with grtc-) obtained from the Guava dashboard.

<CodeBlock filename="index.html" language="html" code={`<script src="https://app.goguava.ai/static/build/webrtc-widgets/guava-widget-audio-orb.js" data-webrtc-code="grtc-YOUR_AGENT_CODE_HERE"

`} />

Complete standalone page

If you don't have a page yet, here's a full HTML file you can use as a starting point:

<CodeBlock filename="index.html" code={`

My Voice Agent `} />

Script tag attributes

<PropTable rows={[ { name: "src", type: "URL", desc: "URL to the hosted widget JS file." }, { name: "data-webrtc-code", type: "string", desc: "The WebRTC agent code (e.g. grtc-nB9oE4...). Obtained from the Guava dashboard." }, ]} />

Widget states

State Behavior User action
idle Ready to call — green indicator Click to call
connecting Pulsing animation, clicks disabled Wait
active Call in progress — audio-reactive visuals Click to hang up
error Red indicator — connection failed Click to retry
No configuration needed. The widget is entirely self-contained — it injects its own HTML and CSS (scoped under `[data-guava-widget]` so it won't conflict with your page styles), wrapped in an IIFE with no global variables. Works on any page with a modern browser.

Generating a WebRTC code via SDK

Instead of obtaining a WebRTC code from the Guava dashboard, you can generate one programmatically with client.create_webrtc_agent(). This is useful when you need to create codes on-the-fly or control their TTL.

<CodeTabs python={{ code: from guava import Client\nfrom datetime import timedelta\n\nclient = Client(api_key="your-api-key")\n\n# Create a WebRTC code valid for 1 hour\nwebrtc_code = client.create_webrtc_agent(ttl=timedelta(hours=1))\nprint(f"WebRTC code: {webrtc_code}")\n\n# Use the code to listen for inbound calls\nclient.listen_inbound(webrtc_code=webrtc_code, controller_class=MyCallController), filename: "generate_code.py" }} typescript={{ code: import * as guava from "@guava-ai/guava-sdk";\n\nconst client = new guava.Client({ apiKey: "your-api-key" });\n\n// Create a WebRTC code valid for 1 hour (3600 seconds)\nconst webrtcCode = await client.createWebrtcAgent({ ttlSec: 3600 });\nconsole.log(\WebRTC code: ${webrtcCode}`);\n\n// Use the code to listen for inbound calls\nclient.listenInbound(\n { webrtc_code: webrtcCode },\n (logger) => new MyCallController(logger),\n);`, filename: "generate_code.ts" }} />

Parameter Type Required Description
ttl datetime.timedelta | None No How long the WebRTC code should remain valid. If omitted, the server default TTL applies.

Returns: str — a WebRTC code (e.g. grtc-...) that can be passed to listen_inbound(webrtc_code=...) or used as a ?webrtc_code=<value> query parameter in the browser widget.

Raises an HTTP error (via check_response) if the API request fails.


import { CodeBlock } from '../views/docs/CodeBlock'; import { Callout, Prose, NextLink } from '../views/docs/prose';

Add a Guava Voice Agent to Your Base44 App

This guide shows you how to add a Guava voice agent to a web app built with Base44. By the end, your Base44 app will have a floating widget that visitors can click to start a spoken conversation with your AI agent. No phone number is required.

You'll need two tools:
Base44 for building your web app
an AI coding platform with terminal access (such as Claude Code or Cursor) for setting up and managing your Guava agent. (The AI coding platform will handle all command-line steps that would otherwise require you to use the terminal.)

Already have a deployed agent and a grtc- code?
Skip to the Quick Start.

New to Guava?
Start with the Full Guide.

Quick Start

You'll need two items ready in order to follow this Quick Start:
  • a deployed Guava agent
  • a WebRTC code (starts with grtc-)
If you do not have both of these yet, follow the Full Guide below.

There are two ways to add the widget to your Base44 app. Choose the one that suits your preference:

  • Script tag approach — The simplest option: no code changes, works in every Base44 project.
  • React component approach — The more configurable option: better if you want to show the widget only on certain pages, or add special functionality. However, it requires Base44 to create a new component and wire it into your app, which adds a small amount of complexity.

Script tag approach

Paste this prompt into Base44's AI chat. Replace grtc-YOUR_CODE_HERE with your actual WebRTC code before pasting:

<CodeBlock language="text" code={`In index.html, add this script tag just before the closing tag:

`}

/>

After Base44 makes the change, the preview will rebuild and a green voice orb will appear in the bottom-right corner of your app. Click it to start a call.

React component approach

Paste this prompt into Base44's AI chat. Replace grtc-YOUR_CODE_HERE with your actual WebRTC code before pasting:

<CodeBlock language="text" code={Create a new component called GuavaVoiceAgent. It should use a useEffect hook to create a script element, set its src to "https://app.goguava.ai/static/build/webrtc-widgets/guava-widget-audio-orb.js", set a data-webrtc-code attribute to "grtc-YOUR_CODE_HERE", append it to the document body, and remove it on cleanup. Then render this component in App.tsx (or the layout component).} />

Full Guide

What is Guava?

Guava is a software development kit (SDK) for creating AI voice agents. In terms of this guide, Guava is a set of tools and resources that is usable via your preferred AI coding platform, that enables you to add an AI voice agent to your app.

What is the voice widget?

The voice widget is a small floating button (called the "orb") that appears on your web app. When a visitor clicks it, their browser opens a live voice conversation with your Guava agent. No phone number or phone call is needed; the conversation happens over Web Real-Time Communication (WebRTC), a technology that enables voice conversations within web apps and pages. The orb is the default styling of the widget. You can restyle the widget to match your app's design, but that's beyond the scope of this guide.

Step 1 — Create a Guava account

Go to app.goguava.ai. Click Get Started in the top right, and create your account.

Step 2 — Create an API key

An API key lets Guava (via the Guava CLI, which you'll install in the next step) connect to your account. To create an API key:
1. In your Guava account, go to the API Keys page.
2. Click + Create API Key at the top right.
3. Name your key something related to your app or voice widget.
Your key will always be available on the API Keys page, so you can copy-paste it whenever you need. Saving a backup in a password manager is good practice, but not required.

Step 3 — Install the Guava CLI

The Guava CLI (Command Line Interface) is the tool that connects your agent code to Guava's platform. Your AI coding platform can operate it for you. You won't need to type terminal commands. (But you could if you want to.) Paste this prompt into your AI coding platform:

<CodeBlock language="text" code={Install the Guava CLI on my machine.} />

Next, connect the CLI to your Guava account. This is a one-time step. Even if you are already signed in to Guava in your browser, the CLI needs its own login. Paste this prompt into your AI coding platform:

<CodeBlock language="text" code={Log in to my Guava account using the Guava CLI.} />

A browser tab will open. Sign in with the account you created in Step 1. Once you sign in, the CLI stores your credentials so you won't have to repeat this step.

Step 4 — Get your agent starter file

Both options below produce a project folder on your computer with a Python file ({'__main__.py'} or main.py) that defines your agent's behavior and goals. You don't need to know Python: once you have the project folder, you can use your preferred AI coding platform to make changes in plain language. Choose the option that best fits your situation: - Start from scratch — This creates a blank starter agent that can hold a conversation, but has not yet been given a specific task, role, or use case. This is a good option if you either prefer to create your agent from scratch, or you want to create a voice agent that's significantly different from all preexisting examples. - Start from an example — This provides you with a ready-made agent that already does something similar to what you want. Best if you'd rather modify an existing agent than build one from scratch. This option requires accessing the Guava examples library public repository on GitHub, but the guide will walk you through those steps.
Start from scratch
Paste the prompt below into your AI coding platform. It will create a project folder called my-agent with starter code that defines a basic voice agent. This starter agent can hold a spoken conversation, but it hasn't been given a specific task, role, or use case. You'll customize that in Step 7.

<CodeBlock language="text" code={Create a new Guava agent project called my-agent.} />

You now have an agent project folder called my-agent. Feel free to rename it to something more personalized, for example: first-webapp-concierge. You'll develop and customize your agent soon. For now, proceed to Step 5.
Start from an example
Guava maintains a library of ready-made example agents organized by industry (healthcare, retail, insurance, and more). You can browse the library, download an agent that's close to what you want, and customize it from there. The examples library is hosted on GitHub, a website where code projects are stored and shared. You don't need a GitHub account, because the Guava example agents library is shared publicly. You'll visit the Guava example library page, browse the available agents, and download the one you want as a ZIP file. Since your voice widget activates when someone clicks it in your app, you'll want an inbound agent, one that responds when a user starts a conversation, rather than an outbound agent that initiates calls. Look for inbound examples when browsing. 1. Browse the examples. Go to github.com/goguava-ai/guava-starter. Click the examples folder to see the list of industries. Click into any industry folder to see the available agents (for example, examples → healthcare → appointment_scheduling). Identify the example agent you'd like to use. 2. Download the library. Once you've found an agent you like, go back to the main page. Click the green Code button near the top right, then click Download ZIP. This downloads the entire examples library as a single ZIP file. Find the downloaded file (usually in your Downloads folder) and double-click it to unzip. GitHub doesn't offer a way to download a single folder, only the entire project. That's why you're downloading the full library. In the next step, you'll copy out just the agent you want. 3. Create your agent project. Copy the agent folder you chose (for example, examples/healthcare/appointment_scheduling/) to a location where you keep your projects. Feel free to rename it to something more personalized, for example: first-webapp-concierge. This copy is your working agent project, the one you'll customize, test, and deploy through the rest of this guide. 4. Connect your AI coding platform. Open your agent project folder in your AI coding platform. This will be how you develop and customize your agent soon. For now, proceed to Step 5.

Step 5 — Create your WebRTC code

A WebRTC code connects the voice widget displayed on your web app to your agent running on Guava's servers. You'll use this code in two places: your agent's code (so it knows to listen for widget connections) and your Base44 app (so the widget knows which agent to connect to). In your Guava account dashboard, go to the WebRTC Codes page and click Create WebRTC Code. Your new code will appear. Copy it and save it somewhere. Unlike the API key from Step 2, this code may not be retrievable later, so make sure you save it now. You'll need it in the next two steps. Advanced: You can also create a WebRTC code using the Guava CLI. See the WebRTC Widgets docs for details.

Step 6 — Connect your WebRTC code to your agent

Your agent needs to know which WebRTC code to use so that visitors who click the widget on your Base44 app connect to the right agent. You'll use the same code again in Step 9 when you add the widget to your Base44 app. Open your agent folder in your preferred AI coding platform and paste this prompt, replacing grtc-YOUR_CODE_HERE with the code you created in Step 5:

<CodeBlock language="text" code={Update my agent to use the WebRTC code grtc-YOUR_CODE_HERE for voice widget connections.} />

Step 7 — Customize and test your agent

Now it's time to design your AI voice agent. Repeat steps 7A and 7B until you're happy with the agent you've created.
Step 7A — Customize your agent
Your agent's behavior and goals are defined in the Python file in your project folder. You can change any of this by describing what you want in plain language to your AI coding platform. Refer your AI coding platform to the Agent docs for the full list of things you can configure in your agent: persona, tasks, conversation flow, and more.
Step 7B — Test your changes
Before deploying your agent, test it on your own computer to make sure it works the way you want. Paste this prompt into your AI coding platform:

<CodeBlock language="text" code={Run my Guava agent locally so I can test it.} />

Your AI coding platform will start the agent and provide a link that opens a voice conversation in your browser. Try out the conversation flow, check that it asks the right questions and responds the way you intended. If something isn't right, describe the changes you want to your AI coding platform, then test again. Repeat until you have the AI voice agent you want.

Step 8 — Deploy your agent

Deploying sends a copy of your agent to Guava's servers so it can run around the clock and handle real visitors on your website. Your local files stay on your computer; deploying does not remove them. Deploying won't make your agent appear on your Base44 app yet; it only makes your agent available on Guava's servers. You'll make your agent "go live" in Step 9. Paste this prompt into your AI coding platform:

<CodeBlock language="text" code={Deploy my Guava agent.} />

If you want to take your agent offline later (for example, while you make major changes or if you no longer need it), run guava deploy down ./my-agent. You can redeploy at any time with guava deploy up ./my-agent. Replace my-agent with your project folder name if you renamed it.

Step 9 — Add the widget to your Base44 app

At the end of this step, your voice agent will be live on your Base44 app. (Note: This is the point where the Quick Start begins. If you're resuming work from this step, you can keep reading here, or switch to the Quick Start.) There are two ways to add the widget to your Base44 app. Choose the one that suits your preference: - Script tag approach — The simplest option: no code changes, works in every Base44 project. - React component approach — The more configurable option: better if you want to show the widget only on certain pages, or add special functionality. However, it requires Base44 to create a new component and wire it into your app, which adds a small amount of complexity.
Script tag approach
Paste this prompt into Base44's AI chat. Replace grtc-YOUR_CODE_HERE with the WebRTC code you created in Step 5:

<CodeBlock language="text" code={`In index.html, add this script tag just before the closing tag:

`}

/>

React component approach
Paste this prompt into Base44's AI chat. Replace grtc-YOUR_CODE_HERE with your actual WebRTC code before pasting:

Step 10 — Keep improving (optional)

Your agent is live on your Base44 app. As you get feedback from real conversations, you can keep refining it. From your Guava account, you can monitor your agent's status on the Deployments page and review every conversation on the Conversations page.
  • Adjust what your agent says and does — make changes in your AI coding platform and redeploy with guava deploy up ./my-agent (replace my-agent with your project folder name if you renamed it). See the Agent docs for the full API.
  • Add a knowledge base — let your agent answer questions from your own documents. See DocumentQA.
  • Customize the widget — see WebRTC Widgets for widget appearance, states, and advanced configuration.
Troubleshooting

I don't see the voice orb.

Go back to Step 9 and make sure the Base44 chat prompt was applied correctly. In Base44's Code tab, open index.html and confirm the script tag appears just before </body>. Also check that the grtc- code in the script tag matches the code you created in Step 5.

The orb shows a red indicator.

This means the widget cannot connect to your agent. The most common cause is that your agent is no longer deployed. Check its status on the Deployments page in your Guava account. If it shows as inactive, redeploy by following Step 8 again.

The widget loads but the voice connection fails.

If the green orb appears but you hear nothing when you click it, your browser may be blocking the voice connection. Open your browser's developer tools (usually F12 or right-click → Inspect) and check the Console tab for error messages. If you see messages mentioning "CSP" or "Content Security Policy," this means Base44's security settings are preventing the voice connection from reaching Guava's servers. Contact Base44 support to ask about allowing WebRTC connections in your app's security policy.

import { CodeBlock } from '../views/docs/CodeBlock'; import { Callout, Prose, NextLink } from '../views/docs/prose';

Add a Guava Voice Agent to Your Lovable App

This guide shows you how to add a Guava voice agent to a web app built with Lovable. By the end, your Lovable app will have a floating widget that visitors can click to start a spoken conversation with your AI agent. No phone number is required.

You'll need two tools:
Lovable for building your web app
an AI coding platform with terminal access (such as Claude Code or Cursor) for setting up and managing your Guava agent. (The AI coding platform will handle all command-line steps that would otherwise require you to use the terminal.)

Quick Start

You'll need two items ready in order to follow this Quick Start:
  • a deployed Guava agent
  • a WebRTC code (starts with grtc-)
If you do not have both of these yet, follow the Full Guide below.

There are two ways to add the widget to your Lovable app. Choose the one that suits your preference:

  • Script tag approach — The simplest option: no code changes, works in every Lovable project.
  • React component approach — The more configurable option: better if you want to show the widget only on certain pages, or add special functionality. However, it requires Lovable to create a new component and wire it into your app, which adds a small amount of complexity.

Script tag approach

Paste this prompt into Lovable's AI chat. Replace grtc-YOUR_CODE_HERE with your actual WebRTC code before pasting:

<CodeBlock language="text" code={`In index.html, add this script tag just before the closing tag:

`}

/>

After Lovable makes the change, the preview will rebuild and a green voice orb will appear in the bottom-right corner of your app. Click it to start a call.

React component approach

Paste this prompt into Lovable's AI chat. Replace grtc-YOUR_CODE_HERE with your actual WebRTC code before pasting:

Full Guide

What is Guava?

What is the voice widget?

Step 1 — Create a Guava account

Step 2 — Create an API key

An API key lets Guava (via the Guava CLI, which you'll install in the next step) connect to your account.

Step 3 — Install the Guava CLI

Paste this prompt into your AI coding platform:

<CodeBlock language="text" code={Install the Guava CLI on my machine.} />

<CodeBlock language="text" code={Log in to my Guava account using the Guava CLI.} />

Step 4 — Get your agent starter file

Choose the option that best fits your situation:
Start from scratch

<CodeBlock language="text" code={Create a new Guava agent project called my-agent.} />

Start from an example

Step 5 — Create your WebRTC code

A WebRTC code connects the voice widget displayed on your web app to your agent running on Guava's servers. You'll use this code in two places: your agent's code (so it knows to listen for widget connections) and your Lovable app (so the widget knows which agent to connect to).

Step 6 — Connect your WebRTC code to your agent

Your agent needs to know which WebRTC code to use so that visitors who click the widget on your Lovable app connect to the right agent. You'll use the same code again in Step 9 when you add the widget to your Lovable app.

<CodeBlock language="text" code={Update my agent to use the WebRTC code grtc-YOUR_CODE_HERE for voice widget connections.} />

Step 7 — Customize and test your agent

Now it's time to design your AI voice agent. Repeat steps 7A and 7B until you're happy with the agent you've created.
Step 7A — Customize your agent
Step 7B — Test your changes

<CodeBlock language="text" code={Run my Guava agent locally so I can test it.} />

Step 8 — Deploy your agent

Deploying won't make your agent appear on your Lovable app yet; it only makes your agent available on Guava's servers. You'll make your agent "go live" in Step 9. Paste this prompt into your AI coding platform:

<CodeBlock language="text" code={Deploy my Guava agent.} />

Step 9 — Add the widget to your Lovable app

At the end of this step, your voice agent will be live on your Lovable app. There are two ways to add the widget to your Lovable app. Choose the one that suits your preference: - Script tag approach — The simplest option: no code changes, works in every Lovable project. - React component approach — The more configurable option: better if you want to show the widget only on certain pages, or add special functionality. However, it requires Lovable to create a new component and wire it into your app, which adds a small amount of complexity.
Script tag approach
Paste this prompt into Lovable's AI chat. Replace grtc-YOUR_CODE_HERE with the WebRTC code you created in Step 5:

<CodeBlock language="text" code={`In index.html, add this script tag just before the closing tag:

`}

/>

React component approach
Paste this prompt into Lovable's AI chat. Replace grtc-YOUR_CODE_HERE with your actual WebRTC code before pasting:

Step 10 — Keep improving (optional)

Your agent is live on your Lovable app. As you get feedback from real conversations, you can keep refining it. From your Guava account, you can monitor your agent's status on the Deployments page and review every conversation on the Conversations page.
Troubleshooting

I don't see the voice orb.

Go back to Step 9 and make sure the Lovable chat prompt was applied correctly. In Lovable's Code Mode, open index.html and confirm the script tag appears just before </body>. Also check that the grtc- code in the script tag matches the code you created in Step 5.

The orb shows a red indicator.


import { CodeBlock } from '../views/docs/CodeBlock'; import { CodeTabs, LanguageTabs } from '../views/docs/CodeTabs'; import { Callout, Prose } from '../views/docs/prose';

Automated Testing for Guava Agents

<LanguageTabs pythonContent={<>This guide focuses on automated testing. For manual testing, use agent.call_local() to place a test call using your local audio device, or agent.chat() to interact with your agent via text in the terminal.Guava Agent tests are defined in code. You can write them using your existing testing framework, such as pytest or Python's built-in unittest module. This lets you test your Agent end-to-end alongside your Expert's code, using patterns you're already familiar with.</>} typescriptContent={<>This guide focuses on automated testing. For manual testing, use agent.callLocal() to place a test call using your local audio device, or agent.chat() to interact with your agent via text in the terminal.Guava Agent tests are defined in code. You can write them using your existing testing framework, such as Jest or Vitest. This lets you test your Agent end-to-end alongside your Expert's code, using patterns you're already familiar with.</>} />

Test individual Agent handlers with MockCall

export const MOCK_CALL_EX_PY = `import unittest from guava.testing import MockCall

Import a handler from the help desk example.

from guava.examples.help_desk import on_action_request

class TestHandlers(unittest.TestCase): def test_routes_to_sales(self): # We can directly invoke that handler with a mocked Call object. actions = on_action_request(MockCall(), "I want to buy a new sofa") self.assertEqual("sales", actions[0].key)`;

export const MOCK_CALL_EX_TS = `import { MockCall } from "@guava-ai/guava-sdk";

// Import the agent from the help-desk example. import { agent } from "@guava-ai/guava-sdk/examples/help-desk";

test("routes to sales", async () => { // We can directly invoke that handler with a mocked Call object. const suggestion = await agent.handlers.onActionRequest(new MockCall(), "I want to buy a new sofa"); expect(suggestion).toHaveProperty("key", "sales"); });`;

<LanguageTabs pythonContent={<> The most basic way to test Guava Agents is to test individual handlers like on_action_request or on_question. You can do so by constructing a guava.testing.MockCall object and passing it in directly to the handler. </>} typescriptContent={<> The most basic way to test Guava Agents is to test individual handlers like onActionRequest or onQuestion. You can do so by constructing a MockCall object and passing it in directly to the handler via agent.handlers. </>} />

Run a roleplay session and analyze the result

export const ROLEPLAY_EX_PY = `import unittest

Import the agent from the help desk example.

from guava.examples.help_desk import agent

class TestHelpDeskAgent(unittest.TestCase): def test_purchase_roleplay(self): session = agent.roleplay("You are a caller who wants to buy a new dining table.")

Use session.get_transcript() to retrieve the session's transcript.

    print(session.get_transcript())

Use session.evaluate() to assess the Agent's performance against a rubric.

    session.evaluate(
        # These criteria must all be true.
        pass_criteria=["The agent identified itself as working for Clearfield Home & Living."],
        # These criteria must all be false.
        fail_criteria=["The agent directly offered to search the inventory."],
    )

You can assert some values directly on the session object.

    self.assertIn("sales", session.executed_actions)
    self.assertEqual("bot-transfer", session.termination_reason)`;

export const ROLEPLAY_EX_TS = `import { agent } from "@guava-ai/guava-sdk/examples/help-desk";

test("purchase roleplay", async () => { const session = await agent.roleplay("You are a caller who wants to buy a new dining table.");

// Use session.getTranscript() to retrieve the session's transcript. console.log(session.getTranscript());

// Use session.evaluate() to assess the Agent's performance against a rubric. await session.evaluate({ // These criteria must all be true. passCriteria: ["The agent identified itself as working for Clearfield Home & Living."], // These criteria must all be false. failCriteria: ["The agent directly offered to search the inventory."], });

// You can assert some values directly on the session object. expect(session.executedActions).toContain("sales"); expect(session.terminationReason).toBe("bot-transfer"); });`;

<LanguageTabs pythonContent={<> agent.roleplay(prompt="...") runs your agent against a separate LLM that improvises a conversation based on a prompt. The session runs to completion and returns a TestSession object you can assert against. As in a real call, your registered handlers will be invoked by the Agent. The returned session object contains the transcript, useful helpers that can be asserted against, and a session.evaluate function that evaluates the conversation against a rubric. </>} typescriptContent={<> agent.roleplay(prompt) runs your agent against a separate LLM that improvises a conversation based on a prompt. The session runs to completion and returns a TestSession object you can assert against. As in a real call, your registered handlers will be invoked by the Agent. The returned session object contains the transcript, useful helpers that can be asserted against, and a session.evaluate() function that evaluates the conversation against a rubric. </>} />

Patch Agent handlers before testing

In some cases, you may not want every handler to run during a test. Use agent.patch() to create a clone of the Agent where you can override callbacks without modifying the original Agent.

export const PATCH_EX_PY = `import unittest import guava

Import the agent from the help desk example.

from guava.examples.help_desk import agent

class TestHelpDeskAgent(unittest.TestCase): def test_sales_closed(self): # Create a clone of the agent and patch the on_action handler. patched = agent.patch()

@patched.on_action("sales") def patched_sales(call: guava.Call): call.hangup( "Tell the caller that the sales department is closed and that " "they should call back tomorrow between 9am and 5pm." )

Run the roleplay test with our patched agent.

    session = patched.roleplay("You are a caller who wants to buy a new dining table.")
    session.evaluate(["The agent informed the caller of the business hours from 9am to 5pm."])`;

export const PATCH_EX_TS = `import type { Call } from "@guava-ai/guava-sdk"; import { agent } from "@guava-ai/guava-sdk/examples/help-desk";

test("sales closed", async () => { // Create a clone of the agent and patch the onAction handler. const patched = agent.patch();

patched.onAction("sales", async (call: Call) => { call.hangup( "Tell the caller that the sales department is closed and that " + "they should call back tomorrow between 9am and 5pm." ); });

// Run the roleplay test with our patched agent. const session = await patched.roleplay("You are a caller who wants to buy a new dining table."); await session.evaluate({ passCriteria: ["The agent informed the caller of the business hours from 9am to 5pm."], }); });`;

<CodeTabs python={{ code: PATCH_EX_PY, filename: "test_patched_agent.py" }} typescript={{ code: PATCH_EX_TS, filename: "patched_agent.test.ts" }} />

<LanguageTabs pythonContent={For any function that isn't an Agent handler, you can patch it using Python's builtin patching system.} typescriptContent={For any function that isn't an Agent handler, you can patch it using your framework's built-in mocking tools.} />

Run a session with full control

export const SCRIPTED_SESSION_EX_PY = `import unittest

Import the agent from the help desk example.

from guava.examples.help_desk import agent

class TestHelpDeskAgent(unittest.TestCase): def test_purchase_routes_to_sales(self): with agent.test() as session: # Wait until the agent has finished its opening turn. session.wait_for_turn()

Inject a caller utterance and let the agent respond.

        session.say("Hi, I'm looking to make a new purchase.")

Wait until the session ends (transfer, hangup, etc.).

        session.wait_for_end()

The TestSession here is the same type as those returned from roleplay sessions.

    # Assert against any of its values, read the transcript, or use 'session.evaluate(...)'
    self.assertIn("sales", session.executed_actions)
    self.assertEqual("bot-transfer", session.termination_reason)`;

export const SCRIPTED_SESSION_EX_TS = `import { agent } from "@guava-ai/guava-sdk/examples/help-desk";

test("purchase routes to sales", async () => { const session = await agent.test(async (session) => { // Wait until the agent has finished its opening turn. await session.waitForTurn();

// Inject a caller utterance and let the agent respond. session.say("Hi, I'm looking to make a new purchase.");

// Wait until the session ends (transfer, hangup, etc.). await session.waitForEnd(); });

// The TestSession here is the same type as those returned from roleplay sessions. // Assert against any of its values, read the transcript, or use 'session.evaluate(...)' expect(session.executedActions).toContain("sales"); expect(session.terminationReason).toBe("bot-transfer"); });`;

agent.test() gives you a live test session where you have full control over timing and caller utterances. Use this when you need precise control over how the conversation unfolds.

<CodeTabs python={{ code: SCRIPTED_SESSION_EX_PY, filename: "test_help_desk.py" }} typescript={{ code: SCRIPTED_SESSION_EX_TS, filename: "help_desk.test.ts" }} />


import { CodeBlock } from '../views/docs/CodeBlock'; import { LanguageTabs, LanguageAlternate } from '../views/docs/CodeTabs'; import { Callout, Prose, NextLink } from '../views/docs/prose';

Outbound Campaigns

While you can use <LanguageAlternate pythonContent={agent.call_phone("+1...")} typescriptContent={agent.callPhone("+1...")} /> to place singular outbound calls, we recommend using Campaigns when calling multiple contacts.

Campaigns automatically manage missed-call retries, enforce calling windows, and control call concurrency. Since they are persistent resources, you should create one Campaign for each distinct use case and continue adding contacts to it as needed.

While Campaigns are responsible for dispatching calls - you will still need to "attach" an Agent to handle the call itself - this is done using a unique "code" created for each Campaign.

Before you start

You'll need:

  • A Guava account — sign up at app.goguava.ai
  • A phone number — from the Phone Numbers page
  • Outbound permission — outbound dialing requires approval. Fill out the Outbound Dialing Permissions Request form on the Compliance page.

Create a campaign in the dashboard

Open the Campaigns page and click Create Campaign. Configure your campaign name, origin phone numbers, calling windows, and retry settings.

Once created, take note of the campaign code — it starts with gcmp-....

Upload contacts

From the campaign's detail page in the dashboard, upload your contact list. Each contact needs a phone number and any per-call data variables your agent will use (such as a patient name or appointment time).

These variables will be accessible inside your agent callbacks via <LanguageAlternate pythonContent={call.get_variable()} typescriptContent={call.getVariable()} />.

Write your agent

Define an Agent and attach callbacks for each stage of the call.

export const AGENT_PY = `import guava from guava import Agent, Field

agent = Agent( name="Sarah", organization="Valley Dental", purpose="Remind patients about their upcoming dental appointments.", )

Fires at the start of every call. We'll start by using reach_person()

to confirm we're talking to the right person.

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

Fires once reach_person() resolves. outcome="available" means the contact answered.

@agent.on_reach_person def on_reach_person(call: guava.Call, outcome: str): if outcome == "available": # Appointment details are only loaded into the context after the # caller's identity has been confirmed. call.add_info("appointment_details", { "patient_name": call.get_variable("patient_name"), "appointment_date": call.get_variable("appointment_date"), "appointment_time": call.get_variable("appointment_time") })

call.set_task( "confirm_appointment", objective="Confirm the appointment date and time.", checklist=[ Field( key="appointment_response", description="Whether the patient confirms, reschedules, or cancels", field_type="multiple_choice", choices=["confirmed", "reschedule", "cancel"], ), ], ) else: call.hangup()

Fires when all checklist items are resolved.

@agent.on_task_complete("confirm_appointment") def on_confirmed(call: guava.Call): call.hangup("Thank them and end the call.")`;

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

const agent = new guava.Agent({ name: "Sarah", organization: "Valley Dental", purpose: "Remind patients about their upcoming dental appointments.", });

// Fires at the start of every call. We'll start by using reachPerson() // to confirm we're talking to the right person. agent.onCallStart(async (call: guava.Call) => { await call.reachPerson(await call.getVariable("patient_name")); });

// Fires once reachPerson() resolves. outcome="available" means the contact answered. agent.onReachPerson(async (call: guava.Call, outcome: string) => { if (outcome === "available") { // Appointment details are only loaded into the context after the // caller's identity has been confirmed. await call.addInfo("appointment_details", { patient_name: await call.getVariable("patient_name"), appointment_date: await call.getVariable("appointment_date"), appointment_time: await call.getVariable("appointment_time"), });

await call.setTask({ taskId: "confirm_appointment", objective: "Confirm the appointment date and time.", checklist: [ guava.Field({ key: "appointment_response", description: "Whether the patient confirms, reschedules, or cancels", fieldType: "multiple_choice", choices: ["confirmed", "reschedule", "cancel"], }), ], }); } else { await call.hangup(); } });

// Fires when all checklist items are resolved. agent.onTaskComplete("confirm_appointment", async (call) => { await call.hangup("Thank them and end the call."); });`;

<LanguageTabs pythonContent={} typescriptContent={} />

export const TEST_PY = agent.chat(variables={ "patient_name": "Jane Smith", "appointment_date": "Monday, June 23rd", "appointment_time": "2:00 PM", });

export const TEST_TS = await agent.chat({ patient_name: "Jane Smith", appointment_date: "Monday, June 23rd", appointment_time: "2:00 PM", });;

Test your agent

<LanguageTabs pythonContent={<>Before attaching to a campaign, test your agent using agent.call_local() or agent.chat(). Pass a variables dict to simulate per-contact data:</>} typescriptContent={<>Before attaching to a campaign, test your agent using agent.callLocal() or agent.chat(). Pass a variables object to simulate per-contact data:</>} />

export const ATTACH_PY = agent.attach_campaign(campaign_code="gcmp-...");

export const ATTACH_TS = await agent.attachCampaign("gcmp-...");;

Attach and serve the campaign

<LanguageTabs pythonContent={<>Next, run agent.attach_campaign("gcmp-..."). This "attaches" your agent to the Campaign that we previously created.</>} typescriptContent={<>Next, call await agent.attachCampaign("gcmp-..."). This "attaches" your agent to the Campaign that we previously created.</>} />

Review calls in the dashboard

Once your campaign is running, visit the Campaigns page to monitor progress.


import { CodeBlock } from '../views/docs/CodeBlock'; import { Callout, Prose, NextLink } from '../views/docs/prose'; export const AGENTIC_TENACITY_EX = `import os

import guava from guava import Field, Say from guava.campaigns import create_or_update_campaign, Contact from guava.types import OutreachModality

1. Create a campaign with a description (required for Agentic Tenacity)

campaign = create_or_update_campaign( "political-poll-q2-2026", origin_phone_numbers=[os.environ["GUAVA_AGENT_NUMBER"]], calling_windows=[ {"day": day, "start_time": "09:00", "end_time": "17:00"} for day in ["monday", "tuesday", "wednesday", "thursday", "friday"] ], start_date="2026-04-01", max_concurrency=3, max_attempts=2, description="Non-partisan political opinion poll for Q2 2026", )

2. Upload contacts with outreach modalities

campaign.upload_contacts( [ Contact( phone_number="+15551234567", data={"first_name": "Alice", "district": "District 5"}, outreach_modalities=["sms"], ), Contact( phone_number="+15559876543", data={"first_name": "Bob", "district": "District 12"}, outreach_modalities=["sms"], ), ], accepted_terms_of_service=True, )

Or apply outreach modalities globally to all contacts:

campaign.upload_contacts( [ Contact(phone_number="+15551234567", data={"first_name": "Alice"}), Contact(phone_number="+15559876543", data={"first_name": "Bob"}), ], accepted_terms_of_service=True, outreach_modalities=["sms"], # applied to all contacts )`;

Agentic Tenacity

Agentic Tenacity is an extension to Guavadialer that intelligently reaches out to contacts across multiple channels before calling them. Rather than placing blind cold calls, the system warms contacts with pre-call alerts and responds to inbound messages to reschedule, answer questions, or mark contacts as do-not-call.

Current status: SMS is the only supported outreach modality. Email, WhatsApp, and RCS/iMessage support is planned. Only pre-call messages are supported — post-call follow-ups are not yet implemented.

How it works

When you upload contacts with outreach modalities enabled, Guava sends a pre-call message before each call attempt. For example: "Hi! This is a message from Harper Valley. We are doing a political opinion survey, and you should receive a call from us in the next 10–30 minutes. If that is not a good time, please reply with a time that works better for you."

The system automatically handles inbound replies — rescheduling calls, answering general questions about the campaign, or marking contacts as DNC.

Setup

1. Apply for SMS permissions — contact the Guava team to enable SMS for your account. This is currently a manual process via a Google Form.

2. Provide a campaign description — the description parameter in create_or_update_campaign() is required when using Agentic Tenacity. The agent uses it to respond to messages about who is calling and what the campaign is about.

Setting outreach modalities

Modalities can be set in two places. Per-contact settings take priority over global settings.

Per contact: Add an outreach_modalities field to individual contact objects. This controls which modalities are used for that specific contact.

Global shortcut: Pass outreach_modalities to campaign.upload_contacts() to apply the same modalities to all contacts in that batch (unless a contact has its own setting).

If neither is set, no pre-call messages are sent and the campaign behaves as a standard Guavadialer campaign.

Example


import { CodeBlock } from '../views/docs/CodeBlock'; import { Callout, Prose, NextLink } from '../views/docs/prose';

Deployment

Guava Deploy is a managed cloud platform that lets you deploy Guava voice-agent projects without provisioning or managing your own infrastructure. When you run guava deploy up, the CLI packages your project, builds it in the cloud, and launches it for you. No servers to set up, no infrastructure to manage.

Security

Your deployments are secure by default:

  • Isolated environments — Each deployment runs in its own private sandbox, completely separated from other users' workloads.
  • Network protection — Your sandbox can make outbound requests to the internet (e.g. calling APIs), but no one can connect into your sandbox from the outside.
  • Secure credentials — Your API key and phone number are injected securely at runtime and never appear in logs.
  • Dedicated resources — CPU and memory are reserved for your deployment, so performance is consistent.

Step-by-step guide

Prerequisites

- A Guava account - A terminal (macOS, Linux, or WSL on Windows)

Step 1 — Install the CLI

Follow the Quickstart guide to install the Guava CLI.

Step 2 — Log in

This opens your browser for authentication. Once you log in, the CLI is authenticated and all subsequent commands will use your account.

Step 3 — Create a project

The CLI walks you through interactive configuration:

1. Base image — Python version (3.10, 3.11, 3.12, 3.13, or 3.14) 2. Instance tier — choose based on your workload:

Tier CPU Memory Use case
guava-seed 1 core 1Gi Development / testing
guava-fruit 2 cores 2Gi Standard production
guava-tree 4 cores 4Gi High-performance workloads

3. Phone number — optionally buy a number now (or later with guava numbers buy)

This generates the following project structure:

<CodeBlock filename="terminal" language="bash" code={my-agent/ .guava # Project config (project ID, tier, base image, etc.) main.py # Required entry point — your agent code goes here pyproject.toml # Python dependencies PRD.md # Product requirements template README.md # Project readme} />

Important: If you use git, the `.guava` file must be committed and kept up to date. The CLI reads it to identify your project and track deployment state.

Deploying an existing project

If you already have a Python project with a main.py, you don't need to run guava create. Just navigate to your project directory and run:

The CLI will detect that there's no .guava config and ask if you'd like to initialize one. It will then prompt you for a base image and instance tier, generate a .guava file, and proceed with the deploy.

Step 4 — Write your agent code

Edit main.py with your voice-agent logic.

For dependencies, Guava supports several common Python workflows. The build system automatically detects which one you're using based on the files in your project:

Files present What happens
uv.lock + pyproject.toml Installs with uv sync --frozen (locked, reproducible)
poetry.lock + pyproject.toml Installs with uv sync
pyproject.toml (alone) Installs with uv sync
requirements.txt Installs with uv pip install -r requirements.txt
requirements.in Installs with uv pip install -r requirements.in

Step 5 — Deploy

The CLI will:

1. Check for changes — if your code hasn't changed since the last deploy, the build step is skipped automatically. 2. Upload your code to cloud storage. 3. Build a container image with your chosen Python version and dependencies. The CLI shows build progress in the terminal. 4. Launch your sandbox and wait until it's running. The CLI shows the deployment status as it starts up.

To force a full rebuild even if your code hasn't changed:

If a deployment is already running, the CLI will ask whether to reuse or replace it.

Step 6 — Check deployment status

Shows whether your deployment is starting up, running, or has encountered an error.

Step 7 — View logs

<CodeBlock filename="terminal" language="bash" code={`# Runtime logs (default: last 200 lines, max 1000) guava deploy logs guava deploy logs -n 500

Build logs (returns a temporary URL to view full build output)

guava deploy build-logs`} />

Step 8 — List all deployments

Prints a table with columns: NAME, NUMBER, ACTIVE, ID.

Step 9 — Update project configuration

Re-prompts for configuration fields (name, base image, tier) with current values shown as defaults.

Step 10 — Check for code changes

Tells you whether your code has changed since the last deploy.

Step 11 — Tear down

Stops the running sandbox. You can also target a specific task:

<CodeBlock code={guava deploy down --id <task-id>} filename="terminal" language="bash" />

Phone number management

Buy a phone number for your project at any time:

The CLI fetches available numbers, shows you a match, and stores the purchased number in .guava. On the next deploy, the number is passed to your sandbox as the GUAVA_AGENT_NUMBER environment variable.

File caching

If you need to cache a file at runtime, write it to /tmp. Note that /tmp is ephemeral: contents are lost when the sandbox restarts.

Quick reference

For a full list of commands and options, see the CLI Reference.


import { CodeBlock } from '../views/docs/CodeBlock'; import { Callout, Prose, NextLink } from '../views/docs/prose';

CLI Reference

guava is the primary interface for working with Guava. It handles authentication, project scaffolding, local runs, deployment, phone number provisioning, conversation history, org management, outbound campaigns, and WebRTC widget codes. The Guava SDK is installed automatically as part of the project template.

Quick reference

Command Description
guava login [--no-launch-browser] Authenticate via OAuth (browser-based)
guava logout Clear stored credentials
guava create [name] [flags…] Scaffold a new project (see guava create below)
guava run [target] [-- args…] Run the agent locally with uv
guava update [flags…] Update project configuration (replaces deploy update)
guava self-upgrade Upgrade the guava CLI to the latest version
guava deploy up [dir] [--rebuild] Build and deploy
guava deploy down [target] [--id ID] Stop a deployment
guava deploy scale <replicas> [dir] Scale a running deployment
guava deploy status [target] Check deployment state
guava deploy logs [target] [-n N] Runtime logs (default 200, max 1000)
guava deploy build-logs [target] [--id ID] Get build log URL
guava deploy list (alias ls) List all deployments
guava deploy changed [dir] Check whether code changed since last deploy
guava numbers list (alias ls) List owned phone numbers
guava numbers buy [dir] Provision a phone number for this project
guava conversations list [filters…] List conversations (calls)
guava conversations get <call_id> Show conversation details
guava conversations transcript <call_id> Show conversation transcript
guava conversations recording <call_id> [-o path] Download recording (WAV)
guava org / guava org list List organizations you belong to
guava org use [org_id] Switch active org (interactive if omitted)
guava campaigns list (alias ls) List outbound campaigns
guava campaigns show <code> Campaign details and status counts
guava campaigns pause <code> / resume <code> / delete <code> Lifecycle controls
guava campaigns add-contacts <code> <file> [flags…] Upload contacts
guava widget [--ttl SEC] Mint a WebRTC widget code

Command reference

guava login

Authenticates the CLI against Guava via OAuth. Opens your default browser to a sign-in page and stores the resulting credentials locally so subsequent commands can call the API on your behalf.

Name Type Required Description
--no-launch-browser flag No Print the login URL instead of opening a browser.

guava logout

Clears stored credentials. No parameters.

guava create

Scaffolds a new Guava project on disk — generates main.py, pyproject.toml, PRD.md, README.md, and the guava.toml config — and (unless --skip-deps is set) runs uv sync to install dependencies. For outbound projects it also creates the matching campaign on the server.

guava create runs in interactive mode by default — the CLI prompts for anything you don't pass. In non-interactive mode (when stdin is not a TTY, or when you pass --no-prompt), there are no prompts: required flags must be supplied or the command errors, and optional flags fall back to documented defaults.

Positional argument

Name Type Required Description
name string No Project path or name. Defaults to current directory.

Non-interactive mode flags — in interactive mode you can omit any of these and the CLI will prompt; in non-interactive mode they behave as noted under "Required".

Name Type Required (non-interactive) Description
--no-prompt flag No Force non-interactive mode. Auto-enabled when stdin is not a TTY.
--direction inbound | outbound Yes Call direction for the project. The only flag that has no default.
--base-image string No Sandbox base image. Default python-sandbox:3.14.
--tier seed | fruit | tree No Instance tier. Default seed.
--replicas integer No Number of pod replicas. Default 1.

Inbound-only (--direction inbound):

Name Type Required (non-interactive) Description
--phone string No Phone source: E.164 number, any (first owned number), or buy (provision a new one).

Outbound-only (--direction outbound):

Name Type Required (non-interactive) Description
--calling-windows JSON Yes E.g. [{"day":"monday","start":"09:00","end":"17:00"}]. No default — must be supplied.
--campaign-name string No Campaign name. Defaults to the project name.
--timezone IANA TZ No Default detected from system.
--origin-number string (repeatable) No E.164, any, or buy. Repeat for multiple.
--max-concurrency integer No Max concurrent calls. Default 1.
--max-attempts integer No Max attempts per recipient. Default 1.
--start-date YYYY-MM-DD No Campaign start date. Default today.
--description string No Free-form description used as outreach context.
--test-phone E.164 No Number that will receive a test call.
Passing an inbound-only flag with `--direction outbound` (or vice-versa) will result in an error.

guava run

Runs the agent locally on your machine. Anything after -- is forwarded straight to main.py, which is useful for one-off flags your agent reads from argv.

Name Type Required Description
target string No Project directory. Defaults to current directory.
-- args… passthrough No Anything after -- is forwarded verbatim to uv run main.py.

guava update

Edits the fields in guava.toml — project name, base image, and instance tier — without re-scaffolding the project. Use this whenever you want to bump the Python sandbox version or move between tiers.

Like create, guava update runs in interactive mode by default and prompts for each field, showing the current value as the default. In non-interactive mode (--no-prompt or no TTY), no prompts run and any field you don't pass simply keeps its current value — nothing is required.

Name Type Required Description
target string No Project directory. Defaults to current directory.

Non-interactive mode flags — in interactive mode the CLI prompts for each of these; in non-interactive mode, any flag you omit leaves the existing value unchanged.

Name Type Required (non-interactive) Description
--no-prompt flag No Force non-interactive mode. Auto-enabled when stdin is not a TTY.
--name string No New project name.
--base-image string No New base image.
--tier seed | fruit | tree No New instance tier.
`guava deploy update` has been removed. Running it will display a message directing you to use `guava update` instead.

guava self-upgrade

Updates the guava binary itself to the latest released version. No parameters.

guava deploy up

Name Type Required Description
dir string No Optional path to the project directory. Defaults to the current directory.
--rebuild flag No Force a fresh build even if code has not changed.

guava deploy down

Name Type Required Description
target string No Optional project name or directory path. Defaults to the current directory.
--id string No Optional task ID to stop directly.

guava deploy scale

Scales an already-running deployment up or down by setting the desired pod-replica count. Use this to handle traffic spikes or scale back when idle without redeploying.

Name Type Required Description
replicas integer Yes Number of pod replicas.
dir string No Project directory. Defaults to current directory.

guava deploy status

Name Type Required Description
target string No Optional project name or directory path. If omitted, behaves like deploy list.

guava deploy logs

Name Type Required Description
target string No Optional project name or directory path. Defaults to the current directory.
-n, --tail integer No Number of log lines to retrieve (default 200, max 1000).

guava deploy build-logs

Name Type Required Description
target string No Optional project name or directory path. Defaults to the current directory.
--id string No Optional Cloud Build ID to look up directly.

guava deploy list

Lists all deployments for the authenticated user as a table (NAME, NUMBER, ACTIVE, ID). ls is an alias for list.

guava deploy changed

Name Type Required Description
target string No Optional project name or directory path. Defaults to the current directory.

Checks whether project files have changed since the last deploy.

guava numbers list

Lists every phone number your active org owns. ls is an alias for list. No parameters.

guava numbers buy

Name Type Required Description
dir string No Optional path to the project directory. Defaults to the current directory.

Purchases a phone number and stores it in guava.toml. On the next deploy, the number is available to your code via the GUAVA_AGENT_NUMBER environment variable.

guava conversations list

Lists past conversations (calls) for your active org, newest first. All filters are optional and can be combined; results are paginated via --after.

Name Type Required Description
--direction inbound | outbound No Filter by call direction.
--from E.164 No Filter by from-number.
--to E.164 No Filter by to-number.
--date-from ISO 8601 date No Earliest start date (e.g. 2026-06-01).
--date-to ISO 8601 date No Latest start date.
-n, --limit integer No Page size (1–100). Default 50.
--after string No Pagination cursor (next_cursor from previous page).

guava conversations get | transcript | recording

Inspect a single past call by ID — get returns the metadata and outcome, transcript returns the speaker-by-speaker text, and recording downloads the raw audio.

All take a positional call_id. recording also accepts -o, --output <path> (default <call_id>.wav).

guava org

Lists the orgs you belong to and lets you switch which one the CLI acts on. The active org determines which numbers, deployments, conversations, and campaigns the other commands see.

Subcommand Description
list (alias ls) List the orgs you belong to. Also the default when no subcommand is given (guava org).
use [org_id] Switch the active org. Omit org_id to pick interactively from the list.

guava campaigns

Manage outbound campaigns from the CLI: inspect their state, pause/resume/delete them, and upload contact lists. Each campaign is referenced by its code, a gcmp--prefixed identifier (e.g. gcmp-xxxxxxxxxxxxxxxxxxxx) shown by guava campaigns list or on the dashboard.

Subcommand Description
list (alias ls) List outbound campaigns.
show <code> Show campaign details and per-status contact counts.
pause <code> Pause an in-flight campaign.
resume <code> Resume a paused campaign.
delete <code> Delete a campaign.
add-contacts <code> <file> [flags…] Upload contacts to a campaign from a file (see below).

add-contacts arguments

Name Type Required Description
code string Yes Campaign code (e.g. gcmp-...).
file path Yes Contacts file. Format dispatched by extension: .csv (header row with phone_number; other columns become string entries in each contact's data), .json (array of objects), .jsonl / .ndjson (one object per line).
--allow-duplicates flag No Upload contacts that are already on the campaign.
--accept-tos flag No Confirm the contacts comply with the Terms of Service. Required in non-interactive mode; otherwise prompted.

Every contact must include a phone_number field in E.164 format (e.g. +15555550100).

guava widget

Mints a short code that lets a browser-based WebRTC widget connect to your deployed agent. Use --ttl for a code that auto-expires; omit it for one that stays valid indefinitely.

Name Type Required Description
--ttl integer (seconds) No TTL for the WebRTC code. Omit for a permanent code.

The guava.toml file

Every Guava project has a guava.toml file in its root directory, created by guava create or guava deploy up. It stores the project's configuration and deployment state. The CLI reads it to identify the project and track deployments.

Projects created with CLI versions 0.29.0 and lower will have a JSON `.guava` file. The CLI will automatically migrate to the new `guava.toml` on the next save. Do not edit this file manually. Use `guava update` to change settings. Manually editing `guava.toml` can put your project into an inconsistent state and cause deploy failures. Commit this file to git. If you delete it or leave it out of version control, the CLI won't be able to find your project or resume previous deployments.

<CodeBlock filename="guava.toml" language="toml" code={project_id = "auto-generated unique ID" name = "your project name" base_image = "python-sandbox:3.14" tier = "guava-seed" # guava-seed | guava-fruit | guava-tree replicas = 1 phone_number = "+15555555555" # set by guava numbers buy or guava create --phone call_direction = "inbound" # inbound | outbound org_id = "..."} />

Instance tiers

Tier CPU Memory Use case
guava-seed 1 1Gi Development / testing
guava-fruit 2 2Gi Standard production
guava-tree 4 4Gi High-performance workloads

Base images & dependencies

When you create a project, you choose a base image. During deployment, your code and dependencies are built on top of this image to produce the final container that runs in the cloud. You can change the base image later with guava update.

Available base images:

  • python-sandbox:3.14
  • python-sandbox:3.13
  • python-sandbox:3.12
  • python-sandbox:3.11
  • python-sandbox:3.10

Dependencies are installed from pyproject.toml, requirements.txt, uv.lock, or poetry.lock.

Troubleshooting

  • "Please run guava login" — Your session has expired or you haven't logged in yet. Run guava login to re-authenticate.
  • "No main.py found" — deploy up requires a main.py in your project directory. Make sure you're running the command from the right folder.
  • Deploy didn't pick up my changes — If nothing changed since the last deploy, the build step is skipped automatically. Use guava deploy up --rebuild to force a full rebuild.
  • Build failed — Run guava deploy build-logs to see the full build output and diagnose the issue.
  • Missing files in deployment — deploy up excludes guava.toml, hidden files/dirs, __pycache__, and node_modules from the upload. Make sure your files aren't in an excluded path.
  • Dependencies not installing — The build system auto-detects dependencies from pyproject.toml, requirements.txt, uv.lock, or poetry.lock. Make sure one of these is present in your project root.

Example

<CodeBlock filename="terminal" language="bash" code={`# Log in (opens browser) guava login

Create a new project

guava create my-agent --direction inbound

Run locally

cd my-agent guava run

Deploy the project

guava deploy up

Scale to 3 replicas

guava deploy scale 3

View runtime logs

guava deploy logs -n 50

Browse recent conversations

guava conversations list --direction inbound -n 20

Download a call recording

guava conversations recording -o call.wav

Tear down

guava deploy down`} />


import { Callout, NextLink } from '../views/docs/prose';

Outbound & SMS Permissions

These registrations get your organization approved to run outbound voice and SMS campaigns through Guava. They're required — you can't make outbound calls or send SMS without them — and several have prerequisite relationships, so the order matters. All registrations are managed from the Guava Compliance page.

Once you're approved, see Phone Number Trust & Reputation for opt-in services that boost campaign deliverability.

Carriers and SMS aggregators require businesses to register before they can send outbound voice or SMS traffic — it's part of how the industry combats spam and fraud. Guava handles the submission and coordination on your behalf; you fill out a form on the Compliance page, and we route it to the right reviewer.

There are four registrations:

Outbound Dialing Registration

Outbound Dialing Registration is the first step in the compliance process and a prerequisite for everything else. This is a Know Your Customer (KYC) form that collects your business identity, point of contact, compliance contact, call setup details, and telecom compliance information.

To get started, open the Guava Compliance page and fill out the Outbound Permissions form under General Forms. Once submitted, the form enters a review queue for the Guava team. Upon approval, your organization will be enabled for outbound dialing, and you'll gain access to the rest of the compliance registrations.

The form requires the submitter to have authority to control or manage the company and bind it to agreements. US-based businesses only; a valid EIN is required.

What you'll need to provide:

  1. Business Identity: Legal business name, business type, industry, EIN, physical address, and website.
  2. Point of Contact: The person authorized to enter into agreements on behalf of the company — name, email, phone, and job title.
  3. Compliance Contact: A contact for compliance-related communications (may be the same as the point of contact).
  4. Call Setup: The types of calls you plan to make (outbound, inbound, WebRTC), a description of your use cases, your phone number plan, and whether your use case involves PHI or cardholder data.
  5. Telecom Compliance: FCC Robocall Mitigation Database (RMD) registration status, TCPA/TSR certification, consent method, Do Not Call Registry validation, and internal DNC list practices.

Use Cases

A Use Case is a named grouping that ties together your campaigns, SMS campaign registration, and analytics provider attestation under a single business purpose. Each use case has a name (e.g., "Collections", "Appointments") and a type selected from a standard list of 45 categories (e.g., Abandoned Cart, Appointment Reminders, Lead Generation, Telehealth, Survey/Research).

Use cases exist because different outbound activities often serve different purposes, and compliance registrations — particularly SMS campaign registration and analytics provider attestation — are scoped per use case. For example, if your organization runs both appointment reminder campaigns and marketing campaigns, those are two separate use cases, each with their own SMS campaign registration and analytics attestation.

To create a use case, go to the Guava Compliance page and click "+ New Use Case" in the Use Cases section. You'll provide a name and select a type. You can then assign your existing campaigns to the use case and submit the SMS Campaign and Analytics Provider forms for it.

You'll need to submit the Outbound Dialing Registration form before you can create use cases.

SMS Brand Registration

SMS Brand Registration registers your business as an SMS brand with the carrier. This is a one-time, organization-wide registration — you only need to complete it once, regardless of how many campaigns or use cases you have. It's a prerequisite for sending SMS messages through Guava.

To register, go to the Guava Compliance page and fill out the SMS Brand Registration form under General Forms.

What you'll need to provide:

  1. Brand Details: Legal company name, DBA/brand name, company type (Public, Private, US Nonprofit, US Government), EIN/Tax ID, and optional alternate identifiers (DUNS, GIIN, LEI).
  2. Address: Street address, city, state, and zip code.
  3. Company Info: Website, and stock symbol/exchange if publicly traded.
  4. Business Contact: First and last name, email, and mobile phone.
  5. Support Contact: Support email and phone number.

Once submitted, your brand registration will be forwarded to the carrier for processing. Upon approval, your organization will be assigned a business ID and your phone numbers will be associated with your brand.

You'll need to submit the Outbound Dialing Registration form before the SMS Brand Registration form becomes available.

SMS Campaign Registration

SMS Campaign Registration is an A2P (Application-to-Person) 10DLC campaign registration, completed once per use case. This registration tells the carrier what kind of SMS messages you'll be sending, how consumers opt in, and what your compliance keyword responses are (e.g., what happens when someone texts STOP or HELP).

To register, go to the Guava Compliance page, find the relevant use case card, and click "SMS Campaign Form."

What you'll need to provide:

  1. Campaign Details: Campaign type (from the use case type list), a description of the SMS campaign, how consumers opt in (call-to-action / message flow), CTA URL, terms of service URL, privacy policy URL, and sample messages.
  2. Compliance Keyword Responses: Your opt-in keywords and response, opt-out (STOP) response, and help (HELP) response. Each response must include your brand name, campaign description, and support contact info.
  3. Usage Disclosures: Whether you use number pooling (50+ numbers), direct lending, embedded links (note: public URL shorteners like bit.ly or tinyurl are not permitted), embedded phone numbers, or age-gated content. You'll also confirm the campaign won't be used for affiliate marketing.
  4. Traffic Profile: SMS traffic type (transactional, customer care, marketing, or mixed), associated website, message frequency, and whether the website/domain is displayed in messages.

Once submitted, the registration will be forwarded to the carrier for processing. Upon approval, SMS will be enabled for your organization and your compliance keyword responses (opt-in, opt-out, help) will be configured.

If you have multiple use cases, you'll complete a separate SMS Campaign Registration for each one.

Summary

Registration Scope Prerequisite Info Needed
Outbound Dialing Registration Once per org — Business identity, EIN, contacts, call setup, telecom compliance
Use Cases One per business purpose Outbound Dialing Registration Name, type
SMS Brand Registration Once per org Outbound Dialing Registration Brand details, address, EIN, business + support contacts
SMS Campaign Registration One per use case SMS Brand Registration + Use Case Campaign details, opt-in flow, compliance responses, usage disclosures, traffic profile

import { Callout, NextLink } from '../views/docs/prose';

Phone Number Trust & Reputation

These are three ways to boost trust and reputation for the phone numbers in your outbound campaigns. None are required to run a campaign — they're all opt-in. Some we handle for you, some we set up on request, and one is a registration you complete yourself through a third-party service.

If you're using your own number rather than a Guava-managed one, trust and reputation registration stays on your side as the number's owner — though we're happy to point you to helpful resources.

Call analytics providers and carriers analyze call metadata to score the phone numbers used in outbound campaigns. Numbers with low reputation scores often get blocked or flagged as spam before they ever reach the recipient. To help your campaigns connect successfully, we recommend three layers of trust and reputation work:

Analytics Provider Registration

Call analytics providers like Transaction Network Services (used by Verizon), First Orion (used by T-Mobile), and Hiya (used by AT&T) analyze call metadata to give phone numbers reputation scores. These reputation scores provide carriers with a probability of the call being spam or fraudulent, and carriers will often block phone numbers with low reputation scores from ever reaching the end user. Registering a business' phone numbers with the analytics providers increases their reputation scores and the likelihood of your calls connecting.

This isn't a service Guava provides directly — registration happens between your organization and the analytics providers. We recommend going through Free Caller Registry, which submits your registration to all three major providers at once.

If you'd like, you can attest that you've completed this registration via the Guava Compliance page under the associated Use Case.

For more general information about analytics providers and reputation scores, you can refer to the resources below:

SHAKEN/STIR Protocol

SHAKEN (or, Signature-based Handling of Asserted Information Using toKENs) / STIR (or, Secure Telephone Identity Revisited) is a framework of interconnected standards, mandated by the FCC to combat the rise in unwanted robocalls and unlawful caller ID spoofing. When adopted, carriers can present a trust indicator, like "Caller Verified," to recipients' phones. You can think about it like a "handshake" that certifies the caller is known and trusted (similar to TLS handshakes and certificate authorities for websites).

We provide SHAKEN/STIR attestation automatically on all Guava-managed numbers — there's nothing to set up on your end.

For more general information about the SHAKEN/STIR Protocol, you can refer to the resources below:

Caller ID Name (CNAM) Registration

CNAM is a feature in the United States public telephone network that identifies an incoming caller by a personal or business name associated with the originating phone number. Your CNAM Display Name, typically a person's name or a company's name, will appear on landline phones by default and mobile phones when enabled by the subscriber.

If you'd like CNAM set up for your Guava-managed phone numbers, reach out to your account representative. We'll handle the registration for you — we just need a few details from your side:

  • Submit the form for each phone number under Phone Numbers on the Guava Compliance Page.
  • If the Caller ID display name doesn't belong to your organization directly, but instead to another organization that has authorized your organization to make outbound calls on its behalf, send your account representative a supporting document (e.g., a signed authorization letter, service agreement, business registration document, or similar) demonstrating that authorization.

Note: If no CNAM is set for a phone number, the default display on the recipient's device will be the City and State associated with the number, along with the number itself.

For more general information about CNAM, you can refer to the resources below:

Summary

Trust Service Who handles it Separate Registrations Info Needed (if you opt in)
Analytics Provider Registration You, via Free Caller Registry (we recommend) For each use case Use case, # of employees, avg. calls/day, phone numbers for the use case
SHAKEN/STIR Protocol Guava (automatic) — None
CNAM Guava (on request) For each display name Display name, associated phone numbers, possible supporting documentation

import { CodeBlock } from '../views/docs/CodeBlock'; import { Callout, Prose, NextLink } from '../views/docs/prose';

export const AGENT_CODE = `import os import guava

from typing_extensions import override from guava import logging_utils

agent = guava.Agent( purpose="You are a helpful voice agent.", name="Hannah", )

@agent.on_call_start() def on_call_start(call: guava.Call): # Set your first task here using call.set_task(...) pass

if name == "main": logging_utils.configure_logging() agent.listen_phone("+1...") # Replace with your agent's phone number. `;

Deploying to Heroku

Guava agents can be deployed to Heroku. They should be run as a background worker, rather than as part of a web server.

  1. Add guava-sdk to your Heroku app.
  2. Create an API Key on the Guava Dashboard.
  3. Use the Heroku dashboard or CLI to add the key to your environment.
  4. Add the following to your Procfile: guava-worker: python agent.py (or whichever file is your agent’s entrypoint).
  5. Push your code and scale the new worker type.

What follows is a detailed guide.

Add guava-sdk as a dependency

Add the Python package guava-sdk as a dependency using the package manager of your choice.

echo "guava-sdk==0.24.0" >> requirements.txt # If using pip + requirements.txt
uv add guava-sdk # If using uv
poetry add guava-sdk # If using poetry

Add a Guava API Key to the Heroku Environment

Open the API Keys page in the Guava dashboard and click Create API Key. The key should be of the form gva-....

Next, add this key to your Heroku environment, either using the CLI or the Heroku dashboard.

heroku config:set GUAVA_API_KEY="gva-..."

Define your agent

Define your agent. In this example we'll create it in a file agent.py, but you can use anything. Note the entrypoint agent.listen_phone(...) at the bottom - this method attaches your agent to the phone number and does not exit.

Add the worker to your Procfile

Add the following section to your Procfile. This will create a new dyno type called "guava-worker".

# Run the Guava agent in a background process.
guava-worker: python agent.py

Push to Heroku

Commit and push your code to Heroku to trigger a deploy.

git add .
git commit -m "Added Guava agent."
git push heroku main # or master, depending on your setup

Scale the Guava worker up

Scale the newly added worker dyno.

heroku ps:scale guava-worker=1

Call your agent

Now, you should be able to call your agent. You can use heroku logs to check logs for your dynos.


import { CodeBlock } from '../views/docs/CodeBlock'; import { CodeTabs } from '../views/docs/CodeTabs'; import { Callout, NextLink, Prose, PropTable } from '../views/docs/prose';

SIP Integrations

Guava agents can receive inbound calls over the SIP protocol. You can use this feature to directly dial Guava agents from an SBC, PBX, or softphone without going out to the PSTN.

Using Twilio? See the [Twilio Programmable Voice guide](/docs/twilio-programmable-voice) or the [Twilio Elastic SIP guide](/docs/twilio-elastic-sip) for step-by-step walkthroughs.

Contact us for peer whitelisting

Currently, we are whitelisting SIP peers at our firewall level. Contact us at hi@goguava.ai to get your source IPs whitelisted.

Check connectivity

Once whitelisted, try to make an OPTIONS ping to our SIP trunk at sip.goguava.ai. If the check fails, see our guide below on firewall configuration.

Create a SIP code

Every SIP integration requires a guavasip code, which you can create in the Guava dashboard. guavasip codes work just like registered phone numbers — agents can listen to them and peers can dial them.

  1. Open the SIP page in the dashboard.
  2. Click Create SIP Code.
  3. Take note of both the SIP code and the termination URI.

<CodeBlock language="bash" code={`# You will see a SIP code like this. guavasip-xxx

Your termination URI will look like this.

sip:guavasip-xxx@sip.goguava.ai`} />

Attach your agent to the SIP code

Next, start an agent using agent.listen_sip("guavasip-xxx"). This is the SIP equivalent of agent.listen_phone(...) — Guava forwards every call addressed to that code to your agent.

<CodeBlock filename="main.py" language="python" code={`import os import guava

agent = guava.Agent( name="Nova", organization="Acme Corp", purpose="Handle inbound calls from the corporate PBX.", )

Register handlers — on_call_received, on_call_start, etc.

Replace with your SIP code from the dashboard.

agent.listen_sip("guavasip-xxx")`} />

To connect an agent to both a phone number and a SIP code simultaneously, see the documentation for guava.Runner.

Dial your agent

The last step is to dial your agent using the termination URI (e.g. sip:guavasip-xxx@sip.goguava.ai). If you're having trouble connecting to your agent, please contact us at hi@goguava.ai.

Firewall configuration

To dial our SIP trunk from your network, you may need to whitelist us in your firewall.

FQDN The FQDN for the Guava SIP trunk is sip.goguava.ai.
Trunk IP The IP address for the Guava SIP trunk is 136.118.29.109. This IP is used for both media and signaling.
TCP Ports 5060, 5061 (TLS)
UDP Ports 5060, 10000-65535 (Media)
ICMP Whitelist ICMP to allow connectivity checks (e.g. ping).
Supported Codecs PCMU (G.711 μ-law), PCMA (G.711 a-law)

import { CodeBlock } from '../views/docs/CodeBlock'; import { Callout, NextLink, Prose, PropTable } from '../views/docs/prose';

Twilio Programmable Voice (TwiML)

Looking for Twilio Elastic SIP? Check out our guide here.

If you already use Twilio Programmable Voice / TwiML, you can transfer to a Guava agent at any time during your call.

  1. Create a Guava SIP code "guavasip-xxx" on the Guava dashboard.
  2. Attach an agent to the SIP code.
  3. Use the following TwiML template to transfer to your agent.

<CodeBlock language="xml" code={<Response> <Dial> <Sip>sip:guavasip-xxx@sip.goguava.ai</Sip> </Dial> </Response>} />

Below is a more detailed guide.

Create a Guava SIP code

Every SIP integration in Guava requires a guavasip code. guavasip codes are used to route inbound calls to agents — agents can listen to codes and peers dial them.

Open the SIP page in the Guava dashboard and click Create SIP Code.

Take note of the SIP code and the termination URI.

Start an agent

Next, start an agent using agent.listen_sip("guavasip-xxx") — Guava forwards every call addressed to that code to your agent.

<CodeBlock filename="main.py" language="python" code={`import os import guava

agent = guava.Agent( name="Nova", organization="Acme Corp", )

Register handlers — on_call_received, on_call_start, etc.

Replace with your SIP code from the dashboard.

agent.listen_sip("guavasip-xxx")`} />

Twilio Example: Outbound Call

Initiate an outbound call through Twilio, then use a Dial command to transfer the call to your Guava agent using the termination URI.

<CodeBlock filename="twilio_outbound.py" language="python" code={`import os from twilio.rest import Client

client = Client( os.environ["TWILIO_ACCOUNT_SID"], os.environ["TWILIO_AUTH_TOKEN"] )

client.calls.create( to="+1...", # The recipient of the call. from_="+1...", # Your owned Twilio number. twiml="sip:guavasip-xxx@sip.goguava.ai", )`} />


Twilio Elastic SIP Integration

Looking for TwiML examples? Check out our Twilio Programmable Voice / TwiML guide.

If you already own a phone number in Twilio and want to connect it to a Guava agent, you can do so using Twilio Elastic SIP Trunking.

Create a Guava SIP code

Every SIP integration in Guava requires a guavasip code. guavasip codes are used to route inbound calls to agents — agents can listen to them and peers can dial them.

Open the SIP page in the Guava dashboard and click Create SIP Code.

Take note of the SIP code and the termination URI.

Create a Twilio Elastic SIP trunk

Navigate to the "Elastic SIP Trunks" page on your Twilio Console. You can find it using the search bar in the top right.

Click Create new SIP Trunk and provide the trunk with a name.

Set the Origination URI

On the trunk's settings page, click Origination on the left hand side. Then click Add new Origination URI and enter your Guava termination URI (e.g. sip:guavasip-xxx@sip.goguava.ai). Then click Add.

Assign a number to the Trunk

Assuming you already have a number in your Twilio account, under the Develop tab, go to Phone Numbers > Manage > Active Numbers and select that number. Select Configure with and change that setting to SIP Trunk. Then select the SIP Trunk dropdown and select the SIP trunk you created earlier.

Finally, scroll down and click Save Configuration.

Start an agent

<CodeBlock filename="main.py" language="python" code={`import os import guava

agent = guava.Agent( name="Nova", organization="Acme Corp", purpose="Handle inbound calls from the corporate PBX.", )

Register handlers — on_call_received, on_call_start, etc.

Replace with your SIP code from the dashboard.

agent.listen_sip("guavasip-xxx")`} />

Call your number

Call your number and you should be able to reach your agent.


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

Cisco CUBE / CUCM

Guava includes features purpose-built to support integration with Cisco enterprise contact center solutions, including Cisco Unified Border Element (CUBE) and Cisco Unified Communications Manager (CUCM).

Email hi@goguava.ai to request an integration guide.


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

AI Customer Service

This example shows how to route calls from an Amazon Connect contact flow to a Guava AI agent. Amazon Connect handles your inbound phone number and initial routing; the Guava agent (Riley) handles the customer conversation — answering questions from a knowledge base, collecting details, and escalating when needed.

Riley can:

  • Answer product questions, return policies, shipping info, and warranty questions in real time using a knowledge base
  • Collect customer details and create an Amazon Connect task for cases that need specialist follow-up
  • Transfer the caller back to a live Connect agent queue for sensitive escalations (complaints, legal, "I want to speak to a human")

How It Works

<MermaidDiagram chart={flowchart TD A([Customer calls Amazon Connect]) --> B["Amazon Connect contact flow\n(greeting, optional IVR routing)"] B -->|Transfer to GUAVA_AGENT_NUMBER| C["Riley — Guava AI<br/>Greets caller · Answers questions · Collects details"] KB([knowledge base]) -.->|on_question| C C --> D{Outcome} D -->|Resolved on call| E[Wrap up call] D -->|Needs follow-up| F[Create Connect task] D -->|Needs live agent| G[Transfer back to Connect queue] } />

Prerequisites

  • Python 3.10 or later
  • A Guava account with an API key and a phone number — sign up at app.goguava.ai
  • An AWS account with an Amazon Connect instance

Step 1: Install Guava

Choose whichever package manager you prefer:

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

Also install boto3 for the Amazon Connect task API:

pip install boto3

Step 2: Set Up Amazon Connect

2a. Create or locate your Connect instance

  1. Open the Amazon Connect console.
  2. Create an instance if you don't have one, or select an existing one.
  3. Note the Instance ID — it's the UUID at the end of the instance ARN:
    arn:aws:connect:us-east-1:123456789012:instance/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                                                    └─────────────── Instance ID ──────┘
    

2b. Create a task contact flow

Riley creates Amazon Connect tasks for cases requiring follow-up. Tasks need a dedicated contact flow — the default queue flow doesn't support them.

  1. In your Connect instance, go to Routing → Contact flows → Create contact flow.
  2. Name it Guava Task Flow (or similar).
  3. Add a Set working queue block and point it to your support queue.
  4. Connect it to a Transfer to queue block, then a Disconnect block.
  5. Save and publish the flow.
  6. Open the flow, click Show additional flow information, and copy the Contact flow ID (UUID at the end of the ARN).

2c. Note your support queue phone number

This is the number Riley will transfer escalations to — your live agent queue's direct dial number in Amazon Connect.

  1. Go to Routing → Phone numbers in your Connect instance.
  2. Use any number claimed to your instance that routes to your support queue, or claim a new one.
  3. Copy the number in E.164 format (e.g., +15551234567).

2d. Create the contact flow that routes to Riley

This is the contact flow your customers actually call into. It transfers them to Riley's Guava number.

  1. Go to Routing → Contact flows → Create contact flow (type: Inbound contact flow).
  2. Name it AI Customer Service.
  3. Build the flow:
   Entry ──▶ Play prompt ──▶ Transfer to phone number ──▶ Disconnect
              "Thanks for              (GUAVA_AGENT_NUMBER)
              calling Pinnacle Gear.
              Please hold."
  • Add a Play prompt block: set the text to a brief hold message (or skip it).
    • Add a Transfer to phone number block:
      • Set Phone number to your Guava agent number (the value of GUAVA_AGENT_NUMBER).
      • Set Transfer type to Softphone transfer or PSTN.
    • Add a Disconnect block on the error branch.
  1. Save and publish the flow.
  2. Assign this flow to an inbound phone number: go to Channels → Phone numbers, select a number, and set its contact flow to AI Customer Service.
**Adding routing logic**: If you want Connect to handle an IVR before routing to Riley, add a **Get customer input** block before the transfer. You can route product questions to Guava and billing escalations to a human queue based on the customer's menu selection.

Step 3: Configure AWS Credentials

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 needs:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["connect:StartTaskContact"],
      "Resource": "arn:aws:connect:*:*:instance/YOUR_INSTANCE_ID/*"
    }
  ]
}

Step 4: Set Environment Variables

# Guava
export GUAVA_API_KEY="your-guava-api-key"
export GUAVA_AGENT_NUMBER="+15551000000"      # Your Guava phone number (Riley's number)

# Amazon Connect
export CONNECT_INSTANCE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export CONNECT_CONTACT_FLOW_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"   # Task flow from Step 2b
export CONNECT_SUPPORT_QUEUE_NUMBER="+15551234567"   # Live agent queue from Step 2c

Step 5: Run Riley

python -m examples.integrations.ccaas.amazon_connect.ai_customer_service

You should see:

INFO:ai_customer_service:Riley is ready — listening for inbound calls on +15551000000

Riley is now live. Call your Amazon Connect number (the one you assigned the AI Customer Service flow to in Step 2d) — Connect will transfer the call to Riley automatically.


How the Code Works

Defining the agent

agent = Agent(
    name="Riley",
    organization="Pinnacle Gear Co.",
    purpose="to help Pinnacle Gear customers with product questions, orders, returns, and warranty support",
)

The Agent is a top-level handle for your AI persona. You attach behavior to it with decorators (@agent.on_call_start, @agent.on_question, @agent.on_task_complete(...)) — each decorator registers a handler that runs when the corresponding event fires on a live call.

Accepting inbound calls

agent.inbound_phone(os.environ["GUAVA_AGENT_NUMBER"]).run()

inbound_phone opens a persistent WebSocket to the Guava server and waits for calls. Each time a call arrives on Riley's number — whether from a customer dialing directly or transferred from Amazon Connect — the agent's registered handlers run on a fresh Call object that represents that conversation.

Kicking off the conversation

@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
    call.set_task(
        "intake",
        objective="...",
        checklist=[
            guava.Say("Thanks for calling Pinnacle Gear. I'm Riley, and I'm here to help. ..."),
            guava.Field(key="customer_name", ...),
            guava.Field(key="inquiry", ...),
        ],
    )

on_call_start runs as soon as the call connects. We use it to set the first task — a named intake step the agent works through, greeting the caller and collecting the fields we need before deciding what to do next.

Answering questions with a knowledge base

@agent.on_question
def on_question(call: guava.Call, question: str) -> str:
    return document_qa.ask(question)

Whenever a caller asks something Riley can't answer from context alone — "What's your return policy?", "Is this jacket machine washable?" — Guava invokes on_question with the question in natural language. DocumentQA retrieves the most relevant chunks from SUPPORT_DOCS and generates an accurate answer. Riley speaks the answer back and continues the conversation without any perceivable delay.

DocumentQA() with no store argument runs in server mode — your documents are uploaded to the Guava server and searched there. The namespace parameter scopes the uploaded documents to this agent so they don't collide with documents from other DocumentQA instances in the same account.

Routing after the conversation

intent_recognizer = IntentRecognizer(
    {
        "live_agent": "Caller wants to speak with a human, agent, manager, or supervisor, or has a complaint, legal threat, or sensitive escalation",
        "follow_up_task": "Caller needs a return, refund, exchange, warranty claim, or has a damaged or wrong item that requires team follow-up",
        "resolved": "Caller's question was answered and no further action is needed",
    }
)

@agent.on_task_complete("intake")
def handle_inquiry(call: guava.Call) -> None:
    inquiry = call.get_field("inquiry", "")
    intent = intent_recognizer.classify(inquiry)

if intent == "live_agent":
        escalate_to_human(call)
    elif intent == "follow_up_task":
        collect_followup_details(call)
    else:
        call.hangup(...)

After the intake task finishes collecting details, the registered @agent.on_task_complete("intake") handler classifies the outcome using IntentRecognizer and routes accordingly. Using an LLM-based classifier handles natural phrasing correctly — "the zipper broke on the first use" maps to follow_up_task even without explicitly matching "broken". The recognizer is instantiated once at module level and reused across calls.

Outcome Example signals Action
Resolved Question answered, no action needed Wrap up and end call
Follow-up Return request, warranty claim Collect email, create Connect task
Live agent "I want to speak to someone", complaint Transfer back to Connect

Transferring back to Amazon Connect

def escalate_to_human(call: guava.Call) -> None:
    call.transfer(
        os.environ["CONNECT_SUPPORT_QUEUE_NUMBER"],
        "Let the customer know you're connecting them with a specialist. "
        "Then transfer the call.",
    )

The second argument to call.transfer gives Riley natural language guidance on what to say before transferring — she'll let the customer know what's happening and transfer when it feels right in the conversation. The customer lands in the Connect support queue as a normal inbound call, which agents handle in the Contact Control Panel (CCP).

Creating a Connect task for follow-up

connect_client.start_task_contact(
    InstanceId=os.environ["CONNECT_INSTANCE_ID"],
    ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
    Name=f"Follow-up Required — {call.get_field('customer_name')}"[:512],
    Description=f"Customer: ...\nEmail: ...\nOrder: ...\nIssue: ..."[:4096],
    Attributes={...},
)

When a caller needs follow-up but doesn't need a live agent right now, Riley collects their email and order number in a second task (follow_up). The @agent.on_task_complete("follow_up") handler then creates a task in Amazon Connect via StartTaskContact. Agents see it in the CCP like any other contact — with the customer's name, issue summary, and email for follow-up.


Customization Ideas

Use your own knowledge base Replace SUPPORT_DOCS with your actual product documentation, policy pages, or FAQs. DocumentQA handles chunking and retrieval automatically — paste in as much text as you need.

Add IVR routing in Connect before transferring Instead of routing all calls to Riley, add a Get customer input block in your Connect flow. Route product questions to Riley's number and billing escalations to a human queue. Riley only handles the calls she's best suited for.

Pass context from Connect to Riley via SIP For more advanced setups, Amazon Connect can transfer calls over a SIP trunk to a Guava SIP endpoint. This lets Connect pass the original Contact ID and other attributes as SIP headers, which the agent can read from the call info. You can then link Riley's Connect task back to the original call using PreviousContactId for full call chain reporting.

Route different inquiry types to different agents Run multiple Guava agents on different agent numbers — one for product support, one for returns, one for technical help. Have Connect's IVR route to the right one based on the customer's menu selection.

Use call info to customize behavior per call The @agent.on_call_received decorator lets you inspect the inbound CallInfo (caller number, SIP headers, etc.) before the call is accepted. Use this to accept or decline calls, or to set per-call variables in on_call_start that change Riley's behavior — for example, routing region-specific callers to region-specific transfer numbers.

Complete Example

import guava
import os
import logging
import json
import boto3
from datetime import datetime

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

logger = logging.getLogger("ai_customer_service")

# ---------------------------------------------------------------------------
# Knowledge base
# Guava answers caller questions from these documents in real time.
# Replace with your own product docs, policies, or FAQs.
# ---------------------------------------------------------------------------

SUPPORT_DOCS = """
Pinnacle Gear Co. — Customer Support Reference

RETURNS & REFUNDS
- We accept returns within 30 days of purchase for unused items in original packaging.
- Items showing signs of use may be exchanged for store credit at our discretion.
- Refunds are processed within 5–7 business days of receiving the returned item.
- Sale items are final sale and cannot be returned or exchanged.
- Start a return at pinnaclegear.com/returns or by calling our support line.

SHIPPING
- Standard shipping (5–7 business days): free on orders over $75, otherwise $7.99.
- Expedited shipping (2–3 business days): $14.99 flat rate.
- Overnight shipping: $29.99. Orders placed before 2 PM ET ship the same day.
- We ship to all 50 US states and Canada. International shipping is not available.

WARRANTY
- All products carry a 1-year limited warranty against manufacturing defects.
- Summit Series backpacks and Trail Pro footwear carry a lifetime warranty.
- Warranty claims require proof of purchase. Email support@pinnaclegear.com.
- Damage from misuse, normal wear, or accidents is not covered.

SIZING
- Apparel follows standard US sizing. See pinnaclegear.com/size-guide.
- Footwear runs true to size; for wide feet, size up by half a size.
- When between sizes, size up for layering or down for an athletic fit.

ORDERS & ACCOUNT
- Track orders at pinnaclegear.com/track using your order number and email.
- To modify or cancel an order, contact us within 1 hour of placing it.
- We accept Visa, Mastercard, Amex, Discover, PayPal, and Pinnacle gift cards.
- Pinnacle Rewards: 1 point per dollar spent; 100 points = $5 reward credit.

PRODUCT CARE
- Machine wash apparel on cold, gentle cycle. Tumble dry on low heat.
- Do not use fabric softener on moisture-wicking or DWR-coated gear.
- Re-apply DWR treatment after 10–15 wash cycles.
- Store sleeping bags loosely in a large cotton sack, never compressed long-term.
"""

document_qa = DocumentQA(documents=SUPPORT_DOCS, namespace="pinnacle-gear-customer-service")

connect_client = boto3.client("connect")

intent_recognizer = IntentRecognizer(
    {
        "live_agent": "Caller wants to speak with a human, agent, manager, or supervisor, or has a complaint, legal threat, or sensitive escalation",
        "follow_up_task": "Caller needs a return, refund, exchange, warranty claim, or has a damaged or wrong item that requires team follow-up",
        "resolved": "Caller's question was answered and no further action is needed",
    }
)

agent = Agent(
    name="Riley",
    organization="Pinnacle Gear Co.",
    purpose=(
        "to help Pinnacle Gear customers with product questions, orders, "
        "returns, and warranty support"
    ),
)

@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
    call.set_task(
        "intake",
        objective=(
            "Help the customer with their inquiry. Answer questions accurately using "
            "the knowledge base. If their issue needs a human specialist or involves "
            "a complaint or legal matter, let them know you'll connect them with someone."
        ),
        checklist=[
            guava.Say(
                "Thanks for calling Pinnacle Gear. I'm Riley, and I'm here to help."
            ),
            guava.Field(
                key="customer_name",
                description="Ask for the customer's name.",
                field_type="text",
                required=True,
            ),
            guava.Field(
                key="inquiry",
                description=(
                    "Understand what the customer needs. Answer their question if you can "
                    "using the knowledge base. If you cannot resolve it — they want a "
                    "return, refund, complaint resolution, or to speak with a person — "
                    "capture what they need in detail."
                ),
                field_type="text",
                required=True,
            ),
        ],
    )

@agent.on_question
def on_question(call: guava.Call, question: str) -> str:
    """Answer caller questions from the knowledge base in real time."""
    return document_qa.ask(question)

if intent == "live_agent":
        escalate_to_human(call)
    elif intent == "follow_up_task":
        collect_followup_details(call)
    else:
        call.hangup(
            "Thank the customer for calling Pinnacle Gear. Ask if there's "
            "anything else you can help with, then wish them a great day."
        )

def escalate_to_human(call: guava.Call) -> None:
    """Transfer the caller to the Amazon Connect support queue."""
    call.transfer(
        os.environ["CONNECT_SUPPORT_QUEUE_NUMBER"],
        "Let the customer know you're connecting them with a Pinnacle Gear "
        "specialist who can take care of them. Then transfer the call.",
    )

def collect_followup_details(call: guava.Call) -> None:
    """Collect contact info so a specialist can follow up via a Connect task."""
    call.set_task(
        "follow_up",
        objective=(
            "Collect the customer's contact details so our team can follow up "
            "and resolve their issue."
        ),
        checklist=[
            guava.Say(
                "I'll have our support team reach out to you directly to sort this out."
            ),
            guava.Field(
                key="email",
                description="Ask for their email address for our team to reach them.",
                field_type="text",
                required=True,
            ),
            guava.Field(
                key="order_number",
                description=(
                    "Ask for their order number if they have one. It's fine if they don't."
                ),
                field_type="text",
                required=False,
            ),
        ],
    )

@agent.on_task_complete("follow_up")
def create_followup_task(call: guava.Call) -> None:
    results = {
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "agent": "Riley",
        "organization": "Pinnacle Gear Co.",
        "use_case": "connect_inbound_ai_customer_service",
        "fields": {
            "customer_name": call.get_field("customer_name"),
            "inquiry": call.get_field("inquiry"),
            "email": call.get_field("email"),
            "order_number": call.get_field("order_number"),
        },
    }
    print(json.dumps(results, indent=2))
    logger.info("Follow-up details captured.")

try:
        connect_client.start_task_contact(
            InstanceId=os.environ["CONNECT_INSTANCE_ID"],
            ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
            Name=f"Follow-up Required — {call.get_field('customer_name', 'Customer')}"[:512],
            Description=(
                f"Customer: {call.get_field('customer_name')}\n"
                f"Email: {call.get_field('email')}\n"
                f"Order: {call.get_field('order_number', 'N/A')}\n"
                f"Issue: {call.get_field('inquiry')}"
            )[:4096],
            References={
                "source": {"Value": "guava_ai_customer_service", "Type": "STRING"},
            },
            Attributes={
                "customer_name": call.get_field("customer_name", ""),
                "email": call.get_field("email", ""),
                "order_number": call.get_field("order_number", ""),
            },
        )
        logger.info("Amazon Connect follow-up task created successfully.")
    except Exception as e:
        logger.error("Failed to create Amazon Connect task: %s", e)

call.hangup(
        "Let the customer know our support team will reach out by email within "
        "one business day. Thank them for their patience and wish them a great day."
    )

if __name__ == "__main__":
    logging_utils.configure_logging()
    logger.info(
        "Riley is ready — listening for inbound calls on %s",
        os.environ.get("GUAVA_AGENT_NUMBER", "(GUAVA_AGENT_NUMBER not set)"),
    )
    agent.inbound_phone(os.environ["GUAVA_AGENT_NUMBER"]).run()

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

Step 1: Install Guava

Choose whichever package manager you prefer:

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:

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.
  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).
**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.

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:

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:

{
  "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

# 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

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

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)

@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.

@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

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

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

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

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

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:

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:

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:

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

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)

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

CSAT Survey

This example shows how to use Guava to call customers after a support interaction and collect a brief satisfaction survey — NPS score, resolution status, and open feedback. The results are written to your logs and posted back into Amazon Connect as a task linked to the original support contact, so the survey responses live alongside the conversation they're about.

The Guava agent (Jamie) can:

  • Reach the right customer using reach_person (handles voicemail and the gatekeeper problem)
  • Politely decline if the customer doesn't want to participate, without forcing them through the survey
  • Collect an NPS score, resolution status, and open-ended feedback in a single conversational task
  • Create an Amazon Connect task linked to the original ContactId via RelatedContactId, so the survey is reportable in Connect's contact search and historical metrics

How It Works

<MermaidDiagram chart={flowchart TD A([Guava calls customer]) --> B{Reach the right person?} B -->|No| C[End call politely] B -->|Yes| D{Willing to participate?} D -->|No| E[Thank them, hang up] D -->|Yes| F[Collect NPS, resolution, feedback] F --> G[Derive NPS category] G --> H[Create linked Connect task] H --> I[Thank customer, end call] } />

Prerequisites

  • Python 3.10 or later
  • A Guava account with an API key and a phone number — sign up at app.goguava.ai
  • An AWS account with an Amazon Connect instance
  • The ContactId of the original support call you want to survey about (Amazon Connect surfaces this in the Contact Control Panel and contact search)

Step 1: Install Guava

Choose whichever package manager you prefer:

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:

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 or locate your Connect instance

2b. Create a contact flow for survey tasks

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 CSAT Task Flow.
  3. Add a Set working queue block and point it at the queue you'd like surveys to be reviewed in (a CX or QA queue is a natural fit).
  4. Add a Transfer to queue block, then a Disconnect block.
  5. Save and publish the flow.
  6. Open the flow, click Show additional flow information, and copy the Contact flow ID (the UUID at the end of the ARN).
**Tip:** If you only want surveys to land somewhere queryable rather than being worked by a live agent, point the flow at a low-priority "review" queue — the task will still appear in Connect's historical reports and contact search.

Step 3: Configure AWS Credentials

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

A minimal IAM policy:

Step 4: Set Environment Variables

# Guava
export GUAVA_API_KEY="your-guava-api-key"
export GUAVA_AGENT_NUMBER="+15551000000"      # Your Guava phone number (Jamie's number)

# Amazon Connect
export CONNECT_INSTANCE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export CONNECT_CONTACT_FLOW_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"   # Survey task flow from Step 2b

Step 5: Run the Example

python -m examples.integrations.ccaas.amazon_connect.csat_survey \
  +15559876543 \
  --name "Jane Doe" \
  --contact-id "11111111-2222-3333-4444-555555555555"

Replace +15559876543 with the customer's phone number, set --name to their full name, and set --contact-id to the Amazon Connect ContactId of the original support call you're following up on.

What to expect:

  • Guava calls the customer and verifies they're the right person
  • Jamie asks if they have a minute for a quick survey
    • If they decline, Jamie thanks them and ends the call gracefully — no pressure
    • If they agree, Jamie collects an NPS score, asks whether the issue was resolved, and invites open-ended feedback
  • The full survey result is printed as JSON to stdout and a Connect task is created linked to the original support contact via RelatedContactId — so the response shows up in contact search and historical reports tied to the conversation it's about
  • If no one picks up, Guava ends the call politely

How the Code Works

Defining the agent and starting the call

agent = Agent(
    name="Jamie",
    organization="Pinnacle Gear Co.",
    purpose=(
        "to conduct a brief customer satisfaction survey about a recent "
        "support experience"
    ),
)

agent.outbound_phone(
    from_number=os.environ["GUAVA_AGENT_NUMBER"],
    to_number=args.phone,
    variables={
        "customer_name": args.name,
        "original_contact_id": args.contact_id,
    },
).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 to all handlers via call.get_variable(key) — here we pass the customer's name (so Jamie can address them) and the original Connect ContactId (so the resulting survey task can be linked back to the support call it's about).

Reaching the right person

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

@agent.on_reach_person
def on_reach_person(call: guava.Call, outcome: str) -> None:
    if outcome == "unavailable":
        call.hangup("We were unable to reach the customer. End the call politely.")
        return
    # otherwise, set up the survey task

reach_person handles the gatekeeper problem — Jamie confirms she's speaking with the intended customer before starting the survey. If a family member or colleague answers, she politely asks for the right person. If the customer truly can't be reached, outcome == "unavailable" and we end the call without leaving a long voicemail (post-hoc surveys are easy to skip; we don't push them).

Asking permission, then collecting the survey

call.set_task(
    "survey",
    objective=(
        "Conduct a short, friendly customer satisfaction survey about the customer's "
        "recent support call with Pinnacle Gear. Keep it conversational and brief."
    ),
    checklist=[
        guava.Say(f"Hi {customer_name}, this is Jamie from Pinnacle Gear. ..."),
        guava.Field(key="willing_to_participate", ..., required=True),
        guava.Field(key="nps_score", ..., field_type="integer", required=False),
        guava.Field(key="issue_resolved", ..., required=False),
        guava.Field(key="feedback", ..., required=False),
    ],
)

set_task gives Jamie a named objective and an ordered checklist. The first Field — willing_to_participate — is the gate. The remaining fields are marked required=False and the descriptions explicitly say "Only ask this if they agreed to participate" — so if the customer declines, Jamie doesn't push them through the rest of the survey. The agent reads the prompts as guidance and adapts its phrasing rather than reciting them verbatim, so the conversation stays natural even though the schema is structured.

Honoring the customer's choice

participation_recognizer = IntentRecognizer(["willing to participate", "not willing to participate"])

@agent.on_task_complete("survey")
def save_survey(call: guava.Call) -> None:
    willing = call.get_field("willing_to_participate", "")
    if participation_recognizer.classify(willing) == "not willing to participate":
        call.hangup(
            "Respect their time and thank them for taking the call. "
            "Wish them a great day."
        )
        return
    # otherwise, save the survey

When the survey task completes, the @agent.on_task_complete("survey") handler runs. Customers respond with all kinds of phrasing — "sure", "now's not a great time", "I'm driving", "yeah I guess so" — so we use IntentRecognizer to map the free-text answer to a clean willing / not willing decision. If they declined, we end the call with a thank-you. The recognizer is instantiated once at module level and reused across calls.

Deriving the NPS category

try:
    nps_int = int(nps_raw) if nps_raw else 0
except (ValueError, TypeError):
    nps_int = 0
if nps_int >= 9:
    nps_category = "promoter"
elif nps_int >= 7:
    nps_category = "passive"
else:
    nps_category = "detractor"

NPS uses three buckets — promoters (9–10), passives (7–8), and detractors (0–6). We compute the category server-side rather than asking the agent to do it, so the categorization is consistent across every call and shows up in the Connect task title and attributes in a form you can filter on.

Linking the survey back to the original contact

connect_client.start_task_contact(
    InstanceId=os.environ["CONNECT_INSTANCE_ID"],
    ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
    Name=f"CSAT Survey — {customer_name} ({nps_category})"[:512],
    Description=f"Customer: ...\nNPS Score: ...\nIssue Resolved: ...\nFeedback: ..."[:4096],
    RelatedContactId=original_contact_id,
    Attributes={
        "customer_name": customer_name,
        "nps_score": str(nps_raw or ""),
        "nps_category": nps_category,
        "issue_resolved": issue_resolved,
    },
)

The key field here is RelatedContactId — passing the original support call's ContactId tells Amazon Connect to link the new survey task to that conversation. In Connect's contact search, you can pivot from the support call to its survey task (and back) without joining tables yourself, and historical metrics that group by RelatedContactId will see them as a single customer interaction with a follow-up survey attached.

The Attributes dict is searchable in Connect's contact search — putting nps_score, nps_category, and issue_resolved here means CX leaders can filter and report on surveys directly inside the Connect console.


Customization Ideas

Trigger surveys automatically after every support call Instead of running this script manually, hook it into your support flow: after a Connect contact ends, fire a Lambda (via EventBridge on Amazon Connect Contact Events) that pulls the customer's name and phone from your CRM and invokes this example with the original ContactId. Surveys go out within minutes of the call ending, while the experience is fresh.

Sample only a fraction of calls Calling every customer is overkill — most teams sample 10–20% of contacts. Add a random gate before invoking this script:

import random
if random.random() < 0.15:
    run_csat_survey(...)

Skip detractors of detractors If the customer was already escalated or marked dissatisfied during the original call (visible in Connect attributes on the original contact), skip the survey or route them to a manager call instead — surveying angry customers can make things worse.

Push results to a data warehouse Replace the print(json.dumps(results, indent=2)) line with a write to your warehouse of choice (BigQuery, Snowflake, Redshift) or push to S3 in JSON Lines for downstream analytics — the Connect task gives you the operational view, the warehouse gives you the analytical view.

Customize the survey per product line Pass a product_line variable into the call and conditionally include extra fields in the checklist — e.g., apparel customers get asked about fit and sizing, footwear customers get asked about comfort and durability. The same agent persona can run very different surveys depending on what the customer just bought.


Complete Example

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("csat_survey")

connect_client = boto3.client("connect")

participation_recognizer = IntentRecognizer(["willing to participate", "not willing to participate"])

agent = Agent(
    name="Jamie",
    organization="Pinnacle Gear Co.",
    purpose=(
        "to conduct a brief customer satisfaction survey about a recent "
        "support experience"
    ),
)

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

@agent.on_reach_person
def on_reach_person(call: guava.Call, outcome: str) -> None:
    if outcome == "unavailable":
        call.hangup("We were unable to reach the customer. End the call politely.")
        return

customer_name = call.get_variable("customer_name")
    call.set_task(
        "survey",
        objective=(
            "Conduct a short, friendly customer satisfaction survey about the customer's "
            "recent support call with Pinnacle Gear. Keep it conversational and brief."
        ),
        checklist=[
            guava.Say(
                f"Hi {customer_name}, this is Jamie from Pinnacle Gear. "
                "We noticed you recently contacted our support team, and I was hoping "
                "to grab one minute of your time for a quick feedback survey. "
                "Your input really helps us improve."
            ),
            guava.Field(
                key="willing_to_participate",
                description=(
                    "Ask if they have about a minute to share some feedback. "
                    "Capture 'yes' or 'no'."
                ),
                field_type="text",
                required=True,
            ),
            guava.Field(
                key="nps_score",
                description=(
                    "On a scale of 1 to 10, how likely are they to recommend Pinnacle Gear "
                    "support to a friend or colleague? Only ask this if they agreed to participate."
                ),
                field_type="integer",
                required=False,
            ),
            guava.Field(
                key="issue_resolved",
                description=(
                    "Ask if their issue was fully resolved during the support call. "
                    "Capture 'yes', 'no', or 'partially'. Only ask if they agreed to participate."
                ),
                field_type="text",
                required=False,
            ),
            guava.Field(
                key="feedback",
                description=(
                    "Ask if there's anything we could have done better, or any other "
                    "comments they'd like to share. Only ask if they agreed to participate."
                ),
                field_type="text",
                required=False,
            ),
        ],
    )

@agent.on_task_complete("survey")
def save_survey(call: guava.Call) -> None:
    customer_name = call.get_variable("customer_name")
    original_contact_id = call.get_variable("original_contact_id")

willing = call.get_field("willing_to_participate", "")
    if participation_recognizer.classify(willing) == "not willing to participate":
        call.hangup(
            "Respect their time and thank them for taking the call. "
            "Wish them a great day."
        )
        return

nps_raw = call.get_field("nps_score")
    issue_resolved = call.get_field("issue_resolved", "unknown")
    feedback = call.get_field("feedback", "No additional feedback provided.")

# Derive NPS category from score.
    try:
        nps_int = int(nps_raw) if nps_raw else 0
    except (ValueError, TypeError):
        nps_int = 0
    if nps_int >= 9:
        nps_category = "promoter"
    elif nps_int >= 7:
        nps_category = "passive"
    else:
        nps_category = "detractor"

results = {
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "agent": "Jamie",
        "organization": "Pinnacle Gear Co.",
        "use_case": "post_call_csat_survey",
        "original_contact_id": original_contact_id,
        "fields": {
            "customer_name": customer_name,
            "nps_score": nps_raw,
            "nps_category": nps_category,
            "issue_resolved": issue_resolved,
            "feedback": feedback,
        },
    }
    print(json.dumps(results, indent=2))
    logger.info("CSAT survey results captured — NPS: %s (%s).", nps_raw, nps_category)

# Create a Connect task linked to the original support call via RelatedContactId.
    # This ties the survey results to the interaction for reporting without altering
    # the original contact's attributes.
    try:
        connect_client.start_task_contact(
            InstanceId=os.environ["CONNECT_INSTANCE_ID"],
            ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
            Name=f"CSAT Survey — {customer_name} ({nps_category})"[:512],
            Description=(
                f"Customer: {customer_name}\n"
                f"NPS Score: {nps_raw}/10 ({nps_category})\n"
                f"Issue Resolved: {issue_resolved}\n"
                f"Feedback: {feedback}"
            )[:4096],
            RelatedContactId=original_contact_id,
            References={
                "source": {"Value": "guava_csat_survey", "Type": "STRING"},
            },
            Attributes={
                "customer_name": customer_name,
                "nps_score": str(nps_raw or ""),
                "nps_category": nps_category,
                "issue_resolved": issue_resolved,
            },
        )
        logger.info("Amazon Connect CSAT task created and linked to contact %s.", original_contact_id)
    except Exception as e:
        logger.error("Failed to create Amazon Connect CSAT task: %s", e)

call.hangup(
        "Thank the customer sincerely for their feedback and time. "
        "Let them know their input helps the team improve. Wish them a great day."
    )

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

parser = argparse.ArgumentParser(
        description=(
            "Outbound post-call CSAT survey for Pinnacle Gear Co. "
            "Collects NPS score and feedback after a support interaction, then creates "
            "a linked Amazon Connect task tied to the original call for reporting."
        )
    )
    parser.add_argument("phone", help="Customer phone number in E.164 format (e.g. +15551234567)")
    parser.add_argument("--name", required=True, help="Full name of the customer")
    parser.add_argument(
        "--contact-id",
        required=True,
        help="Amazon Connect ContactId of the original support call to link the survey to",
    )
    args = parser.parse_args()

logger.info(
        "Calling %s (%s) for CSAT survey — original contact: %s",
        args.name,
        args.phone,
        args.contact_id,
    )

---

<!-- section: amazon-connect-product-support -->

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

## Product Support

This example shows how to route inbound product support calls from Amazon Connect to a Guava AI agent that can answer most questions on its own — pulling from a product FAQ — and create an Amazon Connect task when a real human needs to follow up. Unlike the AI Customer Service example, this one keeps the routing simple (no live transfer back to a queue) and focuses on two outcomes: **resolve the question on the call**, or **collect details and hand off to a specialist asynchronously**.

The Guava agent (**Jordan**) can:
- Greet the caller, take their name, and understand what they need
- Answer product, return, shipping, and warranty questions in real time using a product FAQ knowledge base
- Recognize when an issue requires human follow-up (returns, refunds, complaints, damaged items, manager requests) and collect the customer's email and order number for a specialist to reach out

### How It Works

<MermaidDiagram chart={`flowchart TD
    A([Customer calls Amazon Connect]) --> B["Amazon Connect contact flow\n(greeting, optional IVR)"]
    B -->|Transfer to GUAVA_AGENT_NUMBER| C["Jordan — Guava AI<br/>Greets caller · Takes name · Understands issue"]
    KB([Product FAQ knowledge base]) -.->|on_question| C
    C --> D{Outcome}
    D -->|Question answered| E[Wrap up call]
    D -->|Needs follow-up| F[Collect email + order number]
    F --> G[Create Connect task for specialist]
    G --> H[Promise email follow-up, end call]
`} />

### Prerequisites

### 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:

pip install boto3

Step 2: Set Up Amazon Connect

2a. Create or locate your Connect instance

2b. Create a contact flow for escalation tasks

Jordan creates Amazon Connect tasks for cases that need specialist follow-up. Tasks need a dedicated contact flow — the default queue flow doesn't support them.

  1. In your Connect instance, go to Routing → Contact flows → Create contact flow.
  2. Name it Guava Support Task Flow (or similar).
  3. Add a Set working queue block and point it to your support queue.
  4. Connect it to a Transfer to queue block, then a Disconnect block.
  5. Save and publish the flow.
  6. Open the flow, click Show additional flow information, and copy the Contact flow ID (the UUID at the end of the ARN).

2c. Create the contact flow that routes to Jordan

This is the contact flow your customers actually call into. It transfers them to Jordan's Guava number.

  1. Go to Routing → Contact flows → Create contact flow (type: Inbound contact flow).
  2. Name it Product Support.
  3. Build the flow:
   Entry ──▶ Play prompt ──▶ Transfer to phone number ──▶ Disconnect
              "Thanks for              (GUAVA_AGENT_NUMBER)
              calling Pinnacle Gear.
              Please hold."
  1. Save and publish the flow.
  2. Assign this flow to an inbound phone number: go to Channels → Phone numbers, select a number, and set its contact flow to Product Support.
**No live transfer queue here.** Unlike the AI Customer Service example, this example doesn't transfer escalations back to a live agent — it creates an asynchronous Connect task instead. If you'd rather offer a live transfer for escalations, see the AI Customer Service walkthrough for that pattern.

Step 3: Configure AWS Credentials

The IAM user or role needs:

Step 4: Set Environment Variables

# Guava
export GUAVA_API_KEY="your-guava-api-key"
export GUAVA_AGENT_NUMBER="+15551000000"      # Your Guava phone number (Jordan's number)

# Amazon Connect
export CONNECT_INSTANCE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export CONNECT_CONTACT_FLOW_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"   # Task flow from Step 2b

Step 5: Run Jordan

python -m examples.integrations.ccaas.amazon_connect.product_support

You should see:

INFO:product_support:Jordan is ready — listening for inbound calls on +15551000000

Jordan is now live. Call your Amazon Connect number (the one you assigned the Product Support flow to in Step 2c) — Connect will transfer the call to Jordan automatically.

What to expect:

  • Jordan greets the caller, takes their name, and asks what they need
  • For straightforward questions ("How do I wash my jacket?", "What's your return window?", "Do you ship to Canada?"), Jordan answers from the product FAQ in real time and wraps up the call
  • For requests that need a human (returns, refunds, complaints, damaged items, "can I speak to a manager"), Jordan collects the customer's email and order number, creates a Connect task with the issue summary, and tells the customer a specialist will email them within one business day

How the Code Works

The product FAQ knowledge base

# knowledge_base.py
PRODUCT_FAQ = """
Pinnacle Gear Co. — Product FAQ

RETURNS & REFUNDS
- We accept returns within 30 days of purchase ...

SHIPPING
- Standard shipping (5–7 business days): free on orders over $75 ...

WARRANTY
- All Pinnacle Gear products carry a 1-year limited warranty ...
...
"""

The knowledge base lives in a sibling knowledge_base.py file as a single multi-line string. Keeping it in its own module makes it easy to swap in a longer corpus, edit copy without touching the agent code, or load from a file at startup. There's no chunking or indexing required — DocumentQA handles all of that.

Wiring up the agent

from knowledge_base import PRODUCT_FAQ

document_qa = DocumentQA(documents=PRODUCT_FAQ, namespace="pinnacle-gear-product-support")

agent = Agent(
    name="Jordan",
    organization="Pinnacle Gear Co.",
    purpose=(
        "to help customers with product questions, returns, shipping, "
        "and warranty inquiries"
    ),
)

Agent is the top-level handle for the AI persona. DocumentQA with no store argument runs in server mode — your documents are uploaded to the Guava server and searched there, so you don't need to set up GCP credentials or a vector store yourself. The namespace parameter scopes the upload to this agent so it doesn't collide with documents from other DocumentQA instances in the same account.

DocumentQA is content-addressed: if PRODUCT_FAQ hasn't changed since the last run, nothing is re-uploaded — startup is instant after the first launch.

Accepting inbound calls

agent.inbound_phone(os.environ["GUAVA_AGENT_NUMBER"]).run()

inbound_phone opens a persistent WebSocket to the Guava server and waits for calls. Each time a call arrives on Jordan's number — whether dialed directly or transferred from the Amazon Connect contact flow — the agent's registered handlers run on a fresh Call object that represents that conversation.

Kicking off the conversation

@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
    call.set_task(
        "intake",
        objective="Help the customer with their Pinnacle Gear question. ...",
        checklist=[
            guava.Say("Thank you for calling Pinnacle Gear. My name is Jordan. ..."),
            guava.Field(key="customer_name", ...),
            guava.Field(key="issue_summary", ...),
        ],
    )

on_call_start runs as soon as the call connects. We use it to set the first task — a named intake step Jordan works through, greeting the caller, taking their name, and capturing what they need in their own words. The issue_summary field's description tells Jordan to try to resolve the question first using the knowledge base, and only escalate if she can't — so simple FAQ questions get answered without ever entering an escalation flow.

Answering questions with a knowledge base

@agent.on_question
def on_question(call: guava.Call, question: str) -> str:
    """Answer product questions from the knowledge base in real time."""
    return document_qa.ask(question)

Whenever a caller asks something Jordan can't answer from context alone — "What's your return policy?", "Is this jacket machine washable?", "Do you ship overnight?" — Guava invokes on_question with the question in natural language. DocumentQA.ask retrieves the most relevant chunks from the FAQ and generates an accurate answer. Jordan speaks the answer back and continues the conversation without any perceivable delay.

This decorator is what gives Jordan her FAQ superpowers: she'll never make up a return policy. If the FAQ doesn't cover the question, the answer will reflect that, and the customer naturally drops into the escalation path.

Routing after the conversation

intent_recognizer = IntentRecognizer(
    {
        "escalation_needed": "Customer needs a return, refund, exchange, or complaint resolved, wants to speak with a manager or specialist, or has a damaged or broken item",
        "resolved": "Customer's question was answered and no further action is needed",
    }
)

@agent.on_task_complete("intake")
def handle_outcome(call: guava.Call) -> None:
    issue = call.get_field("issue_summary", "")
    if intent_recognizer.classify(issue) == "escalation_needed":
        collect_escalation_details(call)
    else:
        call.hangup("Thank the customer for calling Pinnacle Gear. ...")

After the intake task finishes, the registered @agent.on_task_complete("intake") handler classifies the outcome using IntentRecognizer and routes accordingly. Using an LLM-based classifier handles natural phrasing correctly — "the seam ripped after one trip" maps to escalation_needed even without explicitly matching "broken" or "return". The recognizer is instantiated once at module level and reused across calls.

Outcome Example signals Action
Resolved FAQ question answered, no action needed Wrap up and end call
Escalation needed Return, refund, complaint, damaged item, manager request Collect email + order number, create Connect task

Collecting follow-up details

def collect_escalation_details(call: guava.Call) -> None:
    call.set_task(
        "escalation",
        objective="Collect the customer's contact details so a specialist can follow up.",
        checklist=[
            guava.Say("I'll have a specialist follow up with you directly to take care of that."),
            guava.Field(key="email", ..., required=True),
            guava.Field(key="order_number", ..., required=False),
        ],
    )

If the issue requires escalation, Jordan starts a second task to collect the email and order number. The order number is required=False because not every issue is order-bound (warranty inquiries on long-owned products, general complaints), and the field description explicitly tells Jordan that "if they don't have one, that's fine" — so she doesn't get stuck pestering the customer for something that doesn't exist.

Creating a Connect task for follow-up

@agent.on_task_complete("escalation")
def create_support_task(call: guava.Call) -> None:
    ...
    connect_client.start_task_contact(
        InstanceId=os.environ["CONNECT_INSTANCE_ID"],
        ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
        Name=f"Support Escalation — {call.get_field('customer_name', 'Customer')}"[:512],
        Description=f"Customer: ...\nEmail: ...\nOrder: ...\nIssue: ..."[:4096],
        Attributes={
            "customer_name": call.get_field("customer_name", ""),
            "email": call.get_field("email", ""),
            "order_number": call.get_field("order_number", ""),
        },
    )
    call.hangup("Let the customer know a specialist will reach out by email ...")

Once the escalation task completes, the @agent.on_task_complete("escalation") handler creates a task in Amazon Connect via StartTaskContact. The task's Description carries the full issue summary, and its Attributes make the customer's name, email, and order number searchable in Connect's contact search — so support agents can pick up the work in the Contact Control Panel just like any other contact, with full context.

The handler also prints the captured fields as JSON for local debugging and ends the call with a promise of email follow-up within one business day.


Customization Ideas

Use your own product documentation Replace PRODUCT_FAQ with your real product manuals, policy pages, or help center articles. DocumentQA handles chunking and retrieval automatically — paste in as much text as you need, or load from files at startup:

faq_text = pathlib.Path("docs/product_faq.md").read_text()
document_qa = DocumentQA(documents=faq_text, namespace="my-product-faq")

Combine with a live-transfer escalation path This example only creates async tasks. If some escalations need a live agent immediately (high-value customers, urgent complaints), add a third intent class — e.g. live_agent — and use call.transfer(...) to send those calls back to a Connect queue, like the AI Customer Service example does.

Look up customer info before answering If you can identify the caller from their phone number (CRM lookup), pass their order history into the call as variables and reference it from on_call_start. Jordan can then say "I see your recent order shipped on Tuesday" without the customer needing to provide an order number.

Add real-time inventory or order lookups The @agent.on_question decorator can call any code, not just DocumentQA. Inspect the question, route product-availability questions to your inventory API, route order-status questions to your OMS, and fall back to the FAQ for everything else.

Pass context from Connect to Jordan via SIP For more advanced setups, Amazon Connect can transfer calls over a SIP trunk to a Guava SIP endpoint. This lets Connect pass the original Contact ID and other attributes as SIP headers, which the agent can read from the call info — and you can link the resulting escalation task back to the original call using RelatedContactId for full call chain reporting (see the CSAT Survey example for the same RelatedContactId pattern).


Complete Example

__main__.py

import guava
import os
import sys
import logging
import json
import pathlib
import boto3
from datetime import datetime

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

logger = logging.getLogger("product_support")

# Load the product FAQ from the sibling knowledge_base module.
sys.path.insert(0, str(pathlib.Path(__file__).parent))
from knowledge_base import PRODUCT_FAQ

# Initialize DocumentQA in server mode — no GCP credentials needed.
# Documents are content-addressed: unchanged FAQ text is never re-uploaded.
document_qa = DocumentQA(documents=PRODUCT_FAQ, namespace="pinnacle-gear-product-support")

connect_client = boto3.client("connect")

intent_recognizer = IntentRecognizer(
    {
        "escalation_needed": "Customer needs a return, refund, exchange, or complaint resolved, wants to speak with a manager or specialist, or has a damaged or broken item",
        "resolved": "Customer's question was answered and no further action is needed",
    }
)

agent = Agent(
    name="Jordan",
    organization="Pinnacle Gear Co.",
    purpose=(
        "to help customers with product questions, returns, shipping, "
        "and warranty inquiries"
    ),
)

@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
    call.set_task(
        "intake",
        objective=(
            "Help the customer with their Pinnacle Gear question. Use the knowledge base "
            "to answer product questions accurately. If the customer needs a return, refund, "
            "or to speak with a specialist, collect their details for follow-up."
        ),
        checklist=[
            guava.Say(
                "Thank you for calling Pinnacle Gear. My name is Jordan. "
                "I can help with product questions, returns, shipping, and warranty. "
                "How can I help you today?"
            ),
            guava.Field(
                key="customer_name",
                description="Ask for the customer's name.",
                field_type="text",
                required=True,
            ),
            guava.Field(
                key="issue_summary",
                description=(
                    "Understand what the customer needs. Answer their question if you can "
                    "using the knowledge base. If you can't resolve it — return request, "
                    "refund, complaint, or request to speak with a specialist — note what "
                    "they need in detail."
                ),
                field_type="text",
                required=True,
            ),
        ],
    )

@agent.on_question
def on_question(call: guava.Call, question: str) -> str:
    """Answer product questions from the knowledge base in real time."""
    return document_qa.ask(question)

@agent.on_task_complete("intake")
def handle_outcome(call: guava.Call) -> None:
    issue = call.get_field("issue_summary", "")
    if intent_recognizer.classify(issue) == "escalation_needed":
        collect_escalation_details(call)
    else:
        call.hangup(
            "Thank the customer for calling Pinnacle Gear. Ask if there's anything "
            "else you can help with. If not, wish them a great day."
        )

def collect_escalation_details(call: guava.Call) -> None:
    call.set_task(
        "escalation",
        objective="Collect the customer's contact details so a specialist can follow up.",
        checklist=[
            guava.Say(
                "I'll have a specialist follow up with you directly to take care of that."
            ),
            guava.Field(
                key="email",
                description="Ask for the customer's email address.",
                field_type="text",
                required=True,
            ),
            guava.Field(
                key="order_number",
                description=(
                    "Ask for their order number if relevant to the issue. "
                    "If they don't have one, that's fine."
                ),
                field_type="text",
                required=False,
            ),
        ],
    )

@agent.on_task_complete("escalation")
def create_support_task(call: guava.Call) -> None:
    results = {
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "agent": "Jordan",
        "organization": "Pinnacle Gear Co.",
        "use_case": "inbound_product_support",
        "fields": {
            "customer_name": call.get_field("customer_name"),
            "issue_summary": call.get_field("issue_summary"),
            "email": call.get_field("email"),
            "order_number": call.get_field("order_number"),
        },
    }
    print(json.dumps(results, indent=2))
    logger.info("Escalation details captured.")

try:
        connect_client.start_task_contact(
            InstanceId=os.environ["CONNECT_INSTANCE_ID"],
            ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
            Name=f"Support Escalation — {call.get_field('customer_name', 'Customer')}"[:512],
            Description=(
                f"Customer: {call.get_field('customer_name')}\n"
                f"Email: {call.get_field('email')}\n"
                f"Order: {call.get_field('order_number', 'N/A')}\n"
                f"Issue: {call.get_field('issue_summary')}"
            )[:4096],
            References={
                "source": {"Value": "guava_product_support", "Type": "STRING"},
            },
            Attributes={
                "customer_name": call.get_field("customer_name", ""),
                "email": call.get_field("email", ""),
                "order_number": call.get_field("order_number", ""),
            },
        )
        logger.info("Amazon Connect support task created successfully.")
    except Exception as e:
        logger.error("Failed to create Amazon Connect task: %s", e)

call.hangup(
        "Let the customer know a specialist will reach out by email within one business "
        "day. Thank them for their patience and wish them a great day."
    )

if __name__ == "__main__":
    logging_utils.configure_logging()
    agent.inbound_phone(os.environ["GUAVA_AGENT_NUMBER"]).run()

knowledge_base.py

This file contains the product FAQ document imported by the main script.

PRODUCT_FAQ = """
Pinnacle Gear Co. — Product FAQ

RETURNS & REFUNDS
- We accept returns within 30 days of purchase for unused items in original packaging.
- Items showing signs of use may be returned for store credit only, at our discretion.
- To start a return, visit pinnaclegear.com/returns or call our support line.
- Refunds are processed within 5–7 business days after we receive the returned item.
- Sale items are final sale and cannot be returned or exchanged.
- Gift recipients can exchange items for equal or lesser value without a receipt.

SHIPPING
- Standard shipping (5–7 business days): free on orders over $75, otherwise $7.99.
- Expedited shipping (2–3 business days): $14.99 flat rate.
- Overnight shipping (next business day): $29.99. Orders placed before 2 PM ET ship same day.
- We ship to all 50 US states and Canada. International shipping is not yet available.
- Orders placed before 2 PM ET on business days ship the same day.

WARRANTY
- All Pinnacle Gear products carry a 1-year limited warranty against manufacturing defects.
- Our Summit Series backpacks and Trail Pro footwear carry a lifetime warranty.
- Warranty claims require proof of purchase. Contact support@pinnaclegear.com to file a claim.
- Normal wear and tear, damage from misuse, or accidental damage are not covered under warranty.

SIZING
- Apparel follows standard US sizing. See our full size guide at pinnaclegear.com/size-guide.
- Footwear runs true to size. For wide feet, we recommend sizing up by half a size.
- Backpack sizing is based on torso length, not height. Measure from your C7 vertebra to your iliac crest.
- When between sizes in apparel, size up for layering and size down for a fitted athletic look.

PRODUCT CARE
- Most Pinnacle apparel is machine washable on cold, gentle cycle. Tumble dry on low heat.
- Do not use fabric softener on moisture-wicking or DWR-coated items — it reduces performance.
- Down insulation should be washed on a delicate cycle and dried on low heat with clean tennis balls.
- Re-apply DWR water repellent treatment (such as Nikwax TX.Direct) after 10–15 wash cycles.
- Store sleeping bags loosely in a large cotton storage sack — never compressed for long periods.
- Clean tents with mild soap and cool water. Never machine wash or put a tent in the dryer.

ORDERS & ACCOUNT
- Track your order at pinnaclegear.com/track using your order number and the email on the order.
- To modify or cancel an order, contact us within 1 hour of placing it. After that, it may have shipped.
- We accept Visa, Mastercard, American Express, Discover, PayPal, and Pinnacle gift cards.
- Pinnacle Rewards members earn 1 point per dollar spent. 100 points = $5 reward credit.
- To create an account or reset your password, visit pinnaclegear.com/account.
"""

Release Notes

What's new in Guava.


July 14, 2026

Guava 0.34.0 brings phone number filtering and navigation enhancements to the dashboard, adds SIP header forwarding to SDK event handlers, and improves CLI authentication feedback.

New Features

  • SIP headers are now included in SDK call event payloads, making them accessible from event handlers.
  • The CLI authentication flow now shows clearer success and error feedback after the OAuth callback.

Improvements

  • The Conversations view in the dashboard can now be filtered by phone number, with the filter state preserved in the URL for shareable, bookmarkable links.
  • Navigating back from a conversation detail page now highlights and scrolls to the previously viewed entry in the list.
  • Call detail pages now display the reason a conversation ended.

Bug Fixes

  • Transcripts now correctly truncate at the point of an interruption, removing speech that was cut off before the caller finished speaking.

July 7, 2026

Guava 0.33.0 streamlines CLI workflows with direct project targeting, establishes agent.roleplay() as the canonical testing method name, and adds automated, voice-based "Do Not Call" detection and flagging for outbound campaigns.

New Features

  • guava deploy and guava update now accept a project_id argument directly. The previous task_id parameter has been removed. See the CLI Reference.
  • agent.roleplay() is now the canonical method name for running simulated test calls, replacing agent.test_roleplay(). The previous name continues to work as an alias. See Agent Testing.
  • Voice-based Do Not Call (DNC) detection is now available for outbound campaigns. Callers who verbally opt out are automatically flagged, and a corresponding SDK event is emitted.

June 30, 2026

Guava 0.32.0 adds outbound campaign support to the TypeScript SDK, refines outbound voicemail detection, and introduces filtering by status for campaign conversations.

New Features

  • The attach_campaign method is now available in the TypeScript SDK, bringing parity with the Python SDK for campaign-based agent workflows.
  • Campaign conversations can now be filtered by status in the dashboard.

Improvements

  • Voicemail detection on outbound calls has been improved, driving greater accuracy of campaign statistics.

Bug Fixes

  • Fixed attach_campaign in the Python SDK not exiting cleanly on Ctrl-C.
  • Fixed datetime fields in the CallAttempt API response not being serialized correctly.

June 24, 2026

Guava 0.31.0 adds DTMF (keypress tones) support to the TypeScript SDK, introduces datetime filtering for campaign calling attempts, and enables bundling of documentation directly into new projects.

New Features

  • on_dtmf is now supported in the TypeScript SDK, enabling agents to handle caller keypresses in TypeScript projects.
  • guava create now downloads the Guava documentation bundle into new projects, giving immediate access to SDK reference material.
  • Campaign attempts can now be filtered by a datetime range, making it easier to query attempt history for specific time windows.

Bug Fixes

  • Fixed the guava login command holding TCP ports open if the OAuth callback fails.

June 16, 2026

Guava 0.30.0 adds a new chat widget supporting audio and text, inbound SMS support, in-CLI management of outbound campaigns, Do Not Call list checks for outbound campaigns, and an updated intent recognizer.

New Features

  • A new multimodal widget (guava-widget-audio-chat) supports audio and text chat within a single session.
  • Agents can now send DTMF digits during a call using call.set_agent_dtmf(enabled=True).
  • on_session_end handlers now receive a termination_reason field describing why the session ended, available in both the Python and TypeScript SDKs.
  • GET /v1/conversations now accepts phone-number query parameters to filter results by caller or callee.
  • Reach-person detection has been updated to v2 with improved accuracy.
  • Agents can read incoming messages via a new inbox API endpoint and the next_sms (Python) / nextSms (TypeScript) SDK methods.
  • Agent.chat mode in the TypeScript SDK enables text-based (non-voice) conversation sessions.
  • MockCall and per-handler unit test support have been added to the TypeScript SDK, allowing agent handlers to be tested in isolation.
  • SDK agents now attach to an existing campaign via campaign_code. Create campaigns through the API or dashboard before starting an agent.
  • Do Not Call (DNC) list support has been added to campaigns; numbers on the list are automatically skipped when dialing.
  • Contacts API v2 adds expanded filtering and batch management capabilities.
  • A new IntentRecognizer class in guava.helpers.llm uses Guava's LLM endpoint for intent recognition.
  • OpenAI-compatible wrappers have been added for document QA and vector store workflows. VertexAI helpers have been renamed to GenAI helpers (guava.helpers.genai).
  • New CLI subcommands: guava contact upload, guava conversations, and guava campaigns.
  • guava login now accepts a --no-launch-browser flag for headless and CI environments.
  • CLI configuration is now stored as a plain TOML file, making it easy to inspect and edit by hand.
  • guava deploy now includes a dashboard view showing the status of active deployments.
  • Campaigns can now be created and edited directly from the dashboard.

Improvements

  • Updated guava create project templates. See the CLI Reference.
  • Outbound dialing and SMS compliance forms have been updated with bug fixes and improved field syncing.
  • A skipDeprecationCheck option has been added to the TypeScript SDK client constructor.

June 9, 2026

Guava 0.29.0 introduces text-based chat sessions, an agent testing framework, CLI self-upgrade, and TypeScript SDK Transport v2.

New Features

  • Agents can now conduct text-based chat sessions in addition to voice calls.
  • An initial agent testing framework has been added to the Python SDK, including guava.testing.MockCall for mocking call interactions.
  • The guava CLI now supports self-upgrade via guava self-upgrade, updating the binary in place. See the CLI Reference.
  • The TypeScript SDK has been updated to Transport v2, with a rewritten socket layer, updated event types, and improved inbound call handling.
  • The TypeScript SDK now supports CLI-based authentication, removing the need to pass API keys explicitly when the guava CLI is in use.
  • The LLM helper (guava.helpers.llm) in the Python SDK now authenticates via CLI credentials.
  • Added outbound scheduling examples to the Python SDK with --local flag support.

Improvements

  • Phone numbers are now displayed and formatted correctly during guava create flows.
  • Improved error handling when agent code throws exceptions during execution.
  • Improved call screener resilience against transient errors in both the Python and TypeScript SDKs.

Bug Fixes

  • Fixed guava deploy up incorrectly defaulting to an inbound deployment when run on an existing project without a .guava configuration file.

June 2, 2026

Guava 0.28.0 brings smarter intent handling, automated SMS follow-up for unanswered campaign calls, and CLI improvements.

New Features

  • Agents now handle ambiguous user intents more robustly. A new intent helper method returns a list of plausible matches, and the on_action_request callback accepts this list to naturally disambiguate with the caller.
  • Agentic campaigns now automatically send an SMS follow-up when a call goes unanswered, informing the recipient of the attempt to reach out.
  • guava run now accepts additional arguments to pass through to the running agent. See the CLI Reference.
  • The TypeScript SDK now supports local calling.

Improvements

  • The WebRTC Helper has been updated to v0.2.0, fixing slow bot audio and Bluetooth headset connection issues.
  • The API and Python SDK now warn when attempting to stop a campaign that has already dispatched calls.

Bug Fixes

  • Fixed a crash triggered by multiple action requests or questions said at once.
  • Fixed a bug where admin users were unable to delete deployments from the dashboard.
  • Fixed an issue where inbound calls could be routed incorrectly.

May 27, 2026

WebRTC support arrives in the TypeScript SDK, agentic tenacity comes to campaigns with SMS, and CLI messaging is refined.

New Features

  • The TypeScript SDK now includes WebRTC support, with an updated property-insurance example to get started.
  • Python SDK examples now open an interactive phone number picker when run in an interactive terminal session.
  • The guava deploy command now provides clearer, more informative output messages.
  • Agentic tenacity is now available for campaigns with SMS enabled.

Bug Fixes & Improvements

  • Updated the quickstart guide with separate install method sections and added agent test and stop steps.
  • The docs section now has its own dedicated navigation bar.
  • Various website fixes and content updates.

May 19, 2026

Guava introduces new and improved tools for measuring outbound calling campaign performance.

New Features

  • A new Campaign Stats Endpoint enables querying of call outcome statistics, with support for filtering by multiple statuses. See the Campaign reference for details.
  • A PowerShell install script has been added for Windows CLI installation.

Bug Fixes & Improvements

  • Voicemail detection accuracy is improved. "Voicemail" as a call termination reason is now reported by agents and tracked in autodialer campaigns, yielding better campaign outcomes data. See set_voicemail_action() for related configuration.
  • The process for approving outbound calling and SMS agent workflows has been streamlined. Read more about requesting outbound permissions here.
  • Various refinements to ASR (speech recognition), TTS (text-to-speech), and background noise handling.

May 12, 2026

The Guava CLI makes gains in utility and flexibility. TTS pronunciation is improved.

New Features

  • guava create and guava update now support non-interactive mode, making it easier for scripts, CI pipelines, and agents to run commands. See the CLI Reference.
  • Added guava numbers list to list phone numbers from the CLI.
  • Added guava run to run your agent project locally. See the CLI Reference.
  • The Guava CLI is now available as a Windows binary. See the CLI Reference.
  • Added get_var / set_var as shorter aliases for get_variable / set_variable in the Guava Agent API.
  • call_info is now accessible directly on the call object.
  • The Python SDK can now authenticate using CLI credentials.

Bug Fixes & Improvements

  • For spelling back words (e.g. "I have that as 'Guava, spelled G-U-A-V-A'"), TTS pronunciation and rate of speech are improved.

May 5, 2026

The Guava Agent API gains voice configuration, DTMF event support, and call ID access. Campaigns now support custom caller IDs for pre-approved accounts.

New Features

  • Added a voice parameter to the Agent class.
  • Exposed call.id for use in agent code.
  • Added support for DTMF events in the Guava Agent API.
  • Pre-approved accounts can set their own number as the caller ID for outbound calls. See the Campaign reference.
  • guava deploy can now use .env files to mount secrets. See the Deployment guide.
  • One login can now access multiple orgs.

Bug Fixes & Improvements

  • Increased robustness of ASR background noise rejection and short utterance handling.
  • Various ease-of-use updates to the Guava CLI.

April 29, 2026

The Guava SIP Trunk goes live, bringing inbound SIP calling support. Agent capabilities and campaign management see significant additions.

New Features

  • The Guava SIP Trunk is now live, enabling agents to receive inbound calls over SIP. See the SIP Integration guide.
  • Voicemail behavior is now configurable via set_voicemail_action().
  • The guava widget CLI command generates embed codes for WebRTC-based voice widget integrations.
  • agent.on_escalate enables agents to hand off calls to another destination. See transfer() for related functionality.
  • set_language_mode() has been added to allow language configuration.
  • The Campaigns API now supports streamlined outbound campaign creation and modification.