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.
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 deploycommand 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
Install the CLI
Install the CLI using one of the supported methods for your platform.
<PlatformTabs
macosContent={<>
brew install goguava-ai/tap/guava} language="bash" />
# Installs to \/.local/bin/guava`
curl -fsSL https://goguava.ai/install.sh | sh/.local/bin/guava`
curl -fsSL https://goguava.ai/install.sh | sh`} language="bash" />
</>}
windowsContent={<>
} language="bash" /> </>} linuxContent={<> <Prose>For Linux and WSL, run the installation shell script.</Prose> <CodeBlock code={# Installs to `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)
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={<>
`# 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" }} />
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.
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" }} />
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" }} />
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" }} />
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" }} />
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" }} />
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
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. |
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 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 |
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.
<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.
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.
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_infoto 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_questionmay be invoked multiple times, for example, if a caller asks a question and then refines it.on_questionmay be invoked speculatively before a caller is done talking.on_questionmay be invoked simultaneously withon_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.
- The caller makes a request — e.g. "I'd like to check the status of my order."
- Guava invokes
on_action_requestwith a summary of the request — e.g. "the customer would like to check the status of their order." - You classify the request and return a
SuggestedAction— e.g.SuggestedAction(key="order_status"). You can use our built-inIntentRecognizerhelper, or build your own intent classifier. ReturnNoneif no action matches the request. - Guava decides whether to execute the action — it may proceed immediately or ask the caller to confirm.
- Guava executes the action — The
on_actionhandler registered under the matching suggested action key is called.
<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 specifictask_id. The handler receives only theCallobject. - Generic form:
@agent.on_task_complete(bare decorator) fires for every completed task. The handler receives theCallobject and thetask_idstring, letting you dispatch on it manually.
TypeError.
on_task_completefires 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(), orcall.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_dtmffires 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), usecall.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()
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()
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" }} />
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" }} />
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" }} />
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
variablesdict tocall_phone()/callPhone() - Campaigns — each contact's
datadict 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
secondaryisNoneor empty, the agent operates inprimaryonly (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
gracevoice has clones for Spanish, French, German, and Italian. If no dedicated clone exists for a voice + language combination, the base voice is used.
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" }} />
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:
- Greets whoever answers and introduces itself (organization + purpose).
- Asks for the contact by name. If someone else answered, asks to speak with or be transferred to the contact.
- Determines availability and records the contact's availability in a
contact_availabilityfield. - Fires
agent.on_reach_personwith 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
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
# 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.
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" }} />
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" }} />
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") |
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)
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)
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. |
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 fromsource_listthat matchquery, capped atmax_results.other_appointments: alternative datetimes to suggest whenmatching_appointmentsis empty, also capped atmax_results.
Edge Cases
- Raises
AssertionErrorifmax_resultsis not anint. - 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_appointmentsmay be non-empty even whenmatching_appointmentsis empty — use it to offer the caller nearby alternatives.
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.
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 (backsGenAIEmbedding/GenAIGeneration)pip install 'guava-sdk[openai]'— OpenAI (backsOpenAIEmbedding/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. |
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.
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. |
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. |
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" }}
/>
import { Callout } from '../views/docs/prose';
Conversations
These endpoints let you retrieve, inspect, and delete conversation data for completed calls.
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
}
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
}
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.
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.
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.
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={`
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 |
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.
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
- a deployed Guava agent
- a WebRTC code (starts with
grtc-)
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
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