<!-- section: architecture-overview -->

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.

<svg viewBox="0 0 760 285" xmlns="http://www.w3.org/2000/svg" className="w-full my-8">
  <defs>
    <marker id="ahd" markerWidth="8" markerHeight="7" refX="7" refY="3.5" orient="auto">
      <polygon points="0 0, 8 3.5, 0 7" fill="rgba(255,255,255,0.25)"/>
    </marker>
  </defs>

<rect x="144" y="47" width="256" height="130" rx="14" fill="#1e1f21" stroke="rgba(255,255,255,0.08)" strokeWidth="1.5"/>
  <text x="272" y="65" textAnchor="middle" fill="#555558" fontSize="9" fontFamily="ui-monospace, monospace" letterSpacing="2">GUAVA CLOUD</text>

<rect x="160" y="72" width="224" height="82" rx="8" fill="#272728" stroke="rgba(26,147,254,0.3)" strokeWidth="1.5"/>
  <text x="272" y="110" textAnchor="middle" fill="#1a93fe" fontSize="14" fontFamily="ui-monospace, monospace" fontWeight="700">Dialog System</text>
  <text x="272" y="132" textAnchor="middle" fill="#555558" fontSize="10" fontFamily="ui-monospace, monospace">audio · STT · LLM · TTS</text>

<circle cx="65" cy="115" r="32" fill="#272728" stroke="rgba(255,255,255,0.1)" strokeWidth="1.5"/>
  
  <path d="M 51,113 C 51,103 58,97 65,97 C 72,97 79,103 79,113" fill="none" stroke="#dadada" strokeWidth="1.8" strokeLinecap="round"/>
  <rect x="47" y="113" width="8" height="12" rx="4" fill="#272728" stroke="#dadada" strokeWidth="1.5"/>
  <rect x="75" y="113" width="8" height="12" rx="4" fill="#272728" stroke="#dadada" strokeWidth="1.5"/>
  <text x="65" y="168" textAnchor="middle" fill="#acacac" fontSize="11" fontFamily="ui-monospace, monospace">Caller</text>

<line x1="97" y1="111" x2="137" y2="111" stroke="#acacac" strokeWidth="1.5"/>
  <polygon points="144,111 137,109 137,113" fill="#acacac"/>
  <line x1="104" y1="119" x2="144" y2="119" stroke="#acacac" strokeWidth="1.5"/>
  <polygon points="97,119 104,117 104,121" fill="#acacac"/>
  <text x="120" y="103" textAnchor="middle" fill="#555558" fontSize="9" fontFamily="ui-monospace, monospace">audio</text>

<line x1="400" y1="111" x2="458" y2="111" stroke="#acacac" strokeWidth="1.5"/>
  <polygon points="465,111 458,109 458,113" fill="#acacac"/>
  <line x1="407" y1="119" x2="465" y2="119" stroke="#acacac" strokeWidth="1.5"/>
  <polygon points="400,119 407,117 407,121" fill="#acacac"/>
  <text x="432" y="103" textAnchor="middle" fill="#555558" fontSize="9" fontFamily="ui-monospace, monospace">WebSocket</text>

<rect x="465" y="72" width="186" height="82" rx="8" fill="#272728" stroke="rgba(255,255,255,0.12)" strokeWidth="1.5"/>
  <text x="558" y="108" textAnchor="middle" fill="#dadada" fontSize="13" fontFamily="ui-monospace, monospace" fontWeight="700">Your Expert</text>
  <text x="558" y="130" textAnchor="middle" fill="#555558" fontSize="10" fontFamily="ui-monospace, monospace">Python · TypeScript · ...</text>

<line x1="558" y1="154" x2="465" y2="207" stroke="rgba(255,255,255,0.2)" strokeWidth="1.2" strokeDasharray="5 3" markerEnd="url(#ahd)"/>
  <line x1="558" y1="154" x2="650" y2="207" stroke="rgba(255,255,255,0.2)" strokeWidth="1.2" strokeDasharray="5 3" markerEnd="url(#ahd)"/>

<rect x="385" y="210" width="160" height="56" rx="8" fill="#1e1f21" stroke="rgba(255,255,255,0.1)" strokeWidth="1"/>
  <text x="465" y="234" textAnchor="middle" fill="#dadada" fontSize="11" fontFamily="ui-monospace, monospace" fontWeight="600">Your Infrastructure</text>
  <text x="465" y="252" textAnchor="middle" fill="#555558" fontSize="9.5" fontFamily="ui-monospace, monospace">local or self-hosted</text>

<text x="559" y="242" textAnchor="middle" fill="#555558" fontSize="9" fontFamily="ui-monospace, monospace">or</text>

<rect x="573" y="210" width="152" height="56" rx="8" fill="#1e1f21" stroke="rgba(26,147,254,0.35)" strokeWidth="1.5"/>
  <text x="649" y="234" textAnchor="middle" fill="#dadada" fontSize="11" fontFamily="ui-monospace, monospace" fontWeight="600">Guava Hosting</text>
  <text x="649" y="252" textAnchor="middle" fill="#555558" fontSize="9.5" fontFamily="ui-monospace, monospace">managed by Guava</text>

<circle r="2.5" fill="#1a93fe">
    <animateMotion dur="1.5s" repeatCount="indefinite" calcMode="linear" path="M 97,111 L 144,111" />
    <animate attributeName="opacity" dur="1.5s" repeatCount="indefinite" keyTimes="0;0.1;0.9;1" values="0;1;1;0" calcMode="linear" />
  </circle>
  <circle r="2.5" fill="#1a93fe">
    <animateMotion dur="1.5s" repeatCount="indefinite" calcMode="linear" begin="-0.75s" path="M 97,111 L 144,111" />
    <animate attributeName="opacity" dur="1.5s" repeatCount="indefinite" begin="-0.75s" keyTimes="0;0.1;0.9;1" values="0;1;1;0" calcMode="linear" />
  </circle>
  <circle r="2.5" fill="#1a93fe">
    <animateMotion dur="1.5s" repeatCount="indefinite" calcMode="linear" path="M 144,119 L 97,119" />
    <animate attributeName="opacity" dur="1.5s" repeatCount="indefinite" keyTimes="0;0.1;0.9;1" values="0;1;1;0" calcMode="linear" />
  </circle>
  <circle r="2.5" fill="#1a93fe">
    <animateMotion dur="1.5s" repeatCount="indefinite" calcMode="linear" begin="-0.75s" path="M 144,119 L 97,119" />
    <animate attributeName="opacity" dur="1.5s" repeatCount="indefinite" begin="-0.75s" keyTimes="0;0.1;0.9;1" values="0;1;1;0" calcMode="linear" />
  </circle>

<circle r="2.5" fill="#1a93fe">
    <animateMotion dur="1.5s" repeatCount="indefinite" calcMode="linear" path="M 400,111 L 465,111" />
    <animate attributeName="opacity" dur="1.5s" repeatCount="indefinite" keyTimes="0;0.1;0.9;1" values="0;1;1;0" calcMode="linear" />
  </circle>
  <circle r="2.5" fill="#1a93fe">
    <animateMotion dur="1.5s" repeatCount="indefinite" calcMode="linear" begin="-0.75s" path="M 400,111 L 465,111" />
    <animate attributeName="opacity" dur="1.5s" repeatCount="indefinite" begin="-0.75s" keyTimes="0;0.1;0.9;1" values="0;1;1;0" calcMode="linear" />
  </circle>
  <circle r="2.5" fill="#1a93fe">
    <animateMotion dur="1.5s" repeatCount="indefinite" calcMode="linear" path="M 465,119 L 400,119" />
    <animate attributeName="opacity" dur="1.5s" repeatCount="indefinite" keyTimes="0;0.1;0.9;1" values="0;1;1;0" calcMode="linear" />
  </circle>
  <circle r="2.5" fill="#1a93fe">
    <animateMotion dur="1.5s" repeatCount="indefinite" calcMode="linear" begin="-0.75s" path="M 465,119 L 400,119" />
    <animate attributeName="opacity" dur="1.5s" repeatCount="indefinite" begin="-0.75s" keyTimes="0;0.1;0.9;1" values="0;1;1;0" calcMode="linear" />
  </circle>
</svg>

### 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](/docs/agent), 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

<ExpertAnimation />

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](./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](./field).
You will receive a callback when your Agent has completed your task and is awaiting more instructions.

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

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

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

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

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

### Deploying your Expert

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

You have two options for deploying your Expert:

- **Your Infrastructure** — deploy to your own servers, VM, or serverless compute platform. You control the environment.
- **Guava Hosting** — push your Expert with a single [`guava deploy`](/docs/cli-reference) command and Guava manages the rest, including horizontal scaling and redundancy.

See the [Deployment guide](/docs/deployment) for a full walkthrough of both options.

### What to read next

- The [Quickstart](/docs/quickstart) shows you how to set up your dev environment and create your first agent.
- The [Example Walkthroughs](/docs/inbound-rag-example) show full Expert implementations for common scenarios, including Q&A and scheduling.
- Once you're comfortable with the basics, the [SDK Reference](/docs/runner) covers every callback and command in detail.

<NextLink section="quickstart" label="Quickstart" />

---

<!-- section: quickstart -->

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

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

### Install the CLI

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

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

### Authenticate the CLI

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

<CodeBlock code="guava login" language="bash" />

### Create an Agent

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

<CodeBlock code="guava create my-agent" language="bash" />

### Test your Agent

<CodeBlock code="guava run ./my-agent" language="bash" />

You can stop your agent by pressing Ctrl-C.

### Deploy your Agent

<CodeBlock code="guava deploy up ./my-agent" language="bash" />

Track status in [Deployments](https://app.goguava.ai/dashboard/deployments). Every call appears in [Conversations](https://app.goguava.ai/dashboard/conversations).

### Stop your agent after deploying

Be sure to stop your agent after deploying.

<CodeBlock code="guava deploy down ./my-agent" language="bash" />

<NextLink section="inbound-rag-example" label="Inbound Example w/ RAG" />

---

<!-- section: sdk-installation -->

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

## Quickstart (SDK-Only)

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

### Create an account

Sign up for an account at [app.goguava.ai](https://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](https://app.goguava.ai/dashboard/api-keys) page. Purchase a phone number using the [Phone Numbers](https://app.goguava.ai/dashboard/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={<>
    <Prose>Examples can be run directly from the Python SDK. You can browse the examples <a href="https://github.com/goguava-ai/python-sdk/tree/main/guava/examples">on GitHub</a>.</Prose>
    <CodeBlock code={  
`# This will attach your agent to a phone number. Dial that number to talk to your agent.
python -m guava.examples.restaurant_waitlist --phone

# Start a test call with a local audio device.
python -m guava.examples.restaurant_waitlist --local

# This will attach your agent to a WebRTC code.
# You can dial it from the browser at https://app.goguava.ai/debug-webrtc
python -m guava.examples.restaurant_waitlist --webrtc

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

# Start a test call with a local audio device.
npx @guava-ai/guava-sdk restaurant-waitlist --local

# This will attach your agent to a WebRTC code.
# You can dial it from the browser at https://app.goguava.ai/debug-webrtc
npx @guava-ai/guava-sdk restaurant-waitlist --webrtc

# Start an in-terminal text-only chat for testing.
npx @guava-ai/guava-sdk restaurant-waitlist --chat`} language="bash" />
  </>}
/>

<NextLink section="inbound-rag-example" label="Inbound Example w/ RAG" />

---

<!-- section: inbound-rag-example -->

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" }}
/>

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

### 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.

<Callout>
  <span className="text-primary font-semibold">Bring your own RAG.</span> The <code>on_question</code> callback receives a plain string and expects a plain string back — you can plug in any knowledge base, vector store, or model you prefer.
</Callout>

### 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" }}
/>

<Callout>
  <span className="text-primary font-semibold">No web servers required.</span> Guava does not require a public web server to receive inbound calls. All Guava agents can be hosted behind firewalls and NATs.
</Callout>

### 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" }}
/>

---

<!-- section: inbound-form-filling -->

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" }}
/>

<Callout>
  If you don't register <code>on_call_received</code>, 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.
</Callout>

### 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" }}
/>

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

### 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" }}
/>

---

<!-- section: outbound-scheduling -->

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" }}
/>

<Callout>
  Under the hood `reach_person()` is just a call to `set_task()`. You can replace `reach_person()` with your own [Task](./tasks) if you need custom behavior here.
</Callout>

### 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" }}
/>

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

### Complete example

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

from guava import logging_utils, Agent
from guava.examples.example_data import MOCK_APPOINTMENTS
from guava.helpers.openai import DatetimeFilter

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

agent = Agent(
    organization="Bright Smile Dental",
    purpose="Call patients to help them schedule a dental appointment.",
)
datetime_filter = DatetimeFilter(source_list=MOCK_APPOINTMENTS)

@agent.on_call_start
def on_call_start(call: guava.Call):
    call.reach_person(
        contact_full_name=call.get_variable("patient_name"),
    )

@agent.on_reach_person
def on_reach_person(call: guava.Call, outcome: str) -> None:
    if outcome == "unavailable":
        call.hangup("Apologize for your mistake and hang up the call.")
    elif outcome == "available":
        call.set_task(
            "schedule_appointment",
            checklist=[
                "Tell them that it's been a while since their regular cleaning with Dr. Teeth.",
                guava.Field(
                    key="appointment_time",
                    field_type="calendar_slot",
                    description="Find a time that works for the caller",
                    searchable=True,
                ),
                "Tell them their appointment has been confirmed and answer any questions before ending the call.",
            ],
        )

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

@agent.on_task_complete("schedule_appointment")
def on_appointment_scheduled(call: guava.Call):
    call.hangup("Thank them for their time and hang up the call.")

if __name__ == "__main__":
    logging_utils.configure_logging()

parser = argparse.ArgumentParser()
    parser.add_argument("phone", type=str, help="Phone number to call.")
    parser.add_argument("name", nargs="?", help="Name of the patient", default="Benjamin Buttons")
    args = parser.parse_args()

agent.call_phone(
        from_number=os.environ["GUAVA_AGENT_NUMBER"],
        to_number=args.phone,
        variables={"patient_name": args.name},
    )`;

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

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

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

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

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

agent.onReachPerson(async (call: guava.Call, outcome: string) => {
  if (outcome === "available") {
    await call.setTask({
      taskId: "schedule_appointment",
      checklist: [
        "Tell them that it's been a while since their regular cleaning with Dr. Teeth.",
        guava.Field({
          key: "appointment_time",
          fieldType: "calendar_slot",
          description: "Find a time that works for the caller",
          searchable: true,
        }),
        "Tell them their appointment has been confirmed and answer any questions before ending the call.",
      ],
    });
  } else {
    await call.hangup("Apologize for your mistake and hang up the call.");
  }
});

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

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

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

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

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

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

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

---

<!-- section: agent -->

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`](./on-caller-speech) | Called each time the caller speaks. |
| [`on_agent_speech`](./on-agent-speech) | Called each time the agent speaks. |
| [`on_question`](./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`](./on-task-complete) | Called when the Agent completes a [Task](./tasks) previously set using `call.set_task`. |
| `on_search_query` | Provide dynamic search results for a searchable [Field](./field). |
| [`on_action_request` / `on_action`](./on-action-request-execute) | Called when the caller asks for a specific action, e.g. "can I reset my password?" |
| [`on_session_end`](./on-session-end) | Called when the session ends. Read `event.termination_reason` to find out why the call ended. |
| [`on_reach_person`](./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](./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. |

<Callout>
  To attach an agent to multiple channels, or run multiple agents in the same process, use <a href="/docs/runner">guava.Runner</a>.
</Callout>

<NextLink section="tasks" label="Tasks" />

---

<!-- section: tasks -->

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 |

<Callout>
  <span className="text-primary font-semibold">guava.Say</span> A <code>guava.Say</code> step is spoken verbatim — use it sparingly when exact wording matters.
</Callout>

### 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" }}
/>

<NextLink section="field" label="Fields" />

---

<!-- section: field -->

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](./tasks) 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` |

<Callout>
  <span className="text-primary font-semibold">Note:</span> The `choices` list for `calendar_slot` fields must be ISO-8601 datetimes (e.g. `"2022-12-31T17:30"`).
</Callout>

<NextLink section="say" label="Say" />

---

<!-- section: runner -->

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.

<CodeBlock code={RUNNER_EX} filename="run.py" language="python" />

### 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." },
]} />

<NextLink section="on-action-request-execute" label="on_action_request() / on_action()" />

---

<!-- section: client -->

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`](./agent) by providing functions for managing account-level resources. It also provides any functions that don't fit onto [`guava.Agent`](./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](./messaging) for details on `send_sms` and `next_sms`.

<NextLink section="messaging" label="SMS Messaging" />

---

<!-- section: messaging -->

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`](./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.

<Callout>
  SMS messaging is available in the <strong>Python and TypeScript SDKs</strong>. For other languages, call the equivalent <a href="/docs/messages-api">Messages REST API</a> directly.
</Callout>

<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.

<Callout>
  Sending SMS requires your organization to complete SMS brand and campaign registration. See <a href="/docs/outbound-and-sms-permissions">Outbound &amp; SMS Permissions</a>.
</Callout>

### 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.

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

<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." },
]} />

<NextLink section="messages-api" label="Messages API" />

---

<!-- section: on-question -->

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](./inbound-rag-example) for a step-by-step walkthrough.

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

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

- `on_question`, like all Guava callbacks, is invoked asynchronously and does not block dialog. The Guava voice agent continues to engage the caller until the question answer comes back.
- `on_question` may be invoked multiple times, for example, if a caller asks a question and then refines it. `on_question` may be invoked speculatively before a caller is done talking.
- `on_question` may be invoked simultaneously with [`on_action_request`](./on-action-request-execute), 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" }}
/>

<NextLink section="on-agent-speech" label="on_agent_speech()" />

---

<!-- section: on-action-request-execute -->

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

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

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

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

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

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

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

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

intent_recognizer = IntentRecognizer(ACTIONS)

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

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

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

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

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

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

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

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

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

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

## on\_action\_request() / on\_action()

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

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

<Callout>
  <span className="text-primary font-semibold">Design note:</span> The two-step pattern (request → execute) gives the agent a chance to confirm intent with the caller before committing to an action.
</Callout>

<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" }}
/>

<NextLink section="on-question" label="on_question()" />

---

<!-- section: on-task-complete -->

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](./tasks) previously set with `call.set_task()` is completed by the agent. Use it to persist collected data, trigger downstream workflows, or move the call to the next stage.

### Signature

There are two forms:

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

- **Per-task form** (recommended): `@agent.on_task_complete("task_name")` binds the handler to a specific `task_id`. The handler receives only the `Call` object.
- **Generic form**: `@agent.on_task_complete` (bare decorator) fires for every completed task. The handler receives the `Call` object and the `task_id` string, letting you dispatch on it manually.

<Callout>
  You cannot mix both forms on the same agent — using per-task handlers alongside a generic handler raises a <code>TypeError</code>.
</Callout>

- `on_task_complete` fires once all checklist items are resolved and the agent has signaled completion.
- Use [`call.get_field()`](./get-field) inside the handler to read values collected during the task.
- The call is still live when this handler runs — you can issue commands such as `call.set_task()`, `call.hangup()`, or `call.transfer()`.

### Example

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

<NextLink section="on-session-end" label="on_session_end()" />

---

<!-- section: on-dtmf -->

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

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

## on\_dtmf()

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

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

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

```python
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`

<NextLink section="on-session-end" label="on_session_end()" />

---

<!-- section: on-session-end -->

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" }}
/>

---

<!-- section: on-agent-speech -->

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()

<Callout>
  <span className="text-primary font-semibold">Rarely needed.</span> This callback fires for every utterance spoken by the agent. Most implementations will not need it and should instead rely on higher-level callbacks like <a href="/docs/on-question"><code>on_question</code></a> or <a href="/docs/on-task-complete"><code>on_task_complete</code></a>. It is most useful for implementing call surveillance or real-time transcription logging.
</Callout>

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`:

```python
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" }}
/>

<NextLink section="on-caller-speech" label="on_caller_speech()" />

---

<!-- section: on-caller-speech -->

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()

<Callout>
  <span className="text-primary font-semibold">Rarely needed.</span> This callback fires for every utterance spoken by the caller. Most implementations will not need it and should instead rely on higher-level callbacks like <a href="/docs/on-question"><code>on_question</code></a> or <a href="/docs/on-task-complete"><code>on_task_complete</code></a>. It is most useful for implementing call surveillance or real-time transcription logging.
</Callout>

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`:

```python
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" }}
/>

<NextLink section="accept-reject" label="accept_call() / reject_call()" />

---

<!-- section: set-task -->

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" }}
/>

<Callout>
  <span className="text-primary font-semibold">Full reference:</span> See the <a href="/docs/tasks" className="text-primary hover:underline">Task</a> page for parameter details, checklist item types, and a complete example.
</Callout>

<NextLink section="set-persona" label="set_persona()" />

---

<!-- section: set-persona -->

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" }}
/>

<Callout>
  <span className="text-primary font-semibold">Tip:</span> Include the contact's name in `agent_purpose` to help the agent personalize the conversation naturally.
</Callout>

<NextLink section="field" label="Field" />

---

<!-- section: send-instruction -->

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" }}
/>

<Callout>
  <span className="text-primary font-semibold">Tip:</span> Unlike `call.set_task()`, `call.send_instruction()` doesn't replace the agent's current objective. Use it to inject context or steer behavior mid-conversation — for example, after a database lookup reveals something the agent should know.
</Callout>

<NextLink section="field" label="Field" />

---

<!-- section: get-set-variable -->

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

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

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

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

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

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

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

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

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

## set\_variable() / get\_variable()

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

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

### Valid variable values

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

### Seeding variables at call start

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

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

### Other ways to store call state

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

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

```python
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" }}
/>

<NextLink section="add-info" label="add_info()" />

---

<!-- section: set-language-mode -->

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

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

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

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

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

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

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

## set\_language\_mode()

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

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

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

The `Language` type is defined as:

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

**Return value:** `None` / `Promise<void>`

### Example: single secondary language

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

### Example: multiple secondary languages

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

### Transcript behavior

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

### Edge cases

- If `secondary` is `None` or empty, the agent operates in `primary` only (the current default behavior).
- When a non-English language is detected during a call, the system automatically switches to a language-specific TTS voice.
- The `grace` voice has clones for Spanish, French, German, and Italian. If no dedicated clone exists for a voice + language combination, the base voice is used.

<Callout>
  <span className="text-primary font-semibold">Compliance:</span> English and Spanish are currently supported for HITRUST / PCI-compliant deployments. All languages other than English and Spanish are not. Support for other languages is planned.
</Callout>

<NextLink section="field" label="Field" />

---

<!-- section: transfer -->

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" }}
/>

<NextLink section="hangup" label="hangup()" />

---

<!-- section: hangup -->

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" }}
/>

<Callout>
  <span className="text-primary font-semibold">Tip:</span> Be specific in your final instructions. The agent will try to fulfill them naturally — including mentioning a confirmation number, scheduling next steps, or expressing appropriate warmth.
</Callout>

<NextLink section="reach-person" label="reach_person()" />

---

<!-- section: reach-person -->

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

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

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

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

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

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

## reach\_person()

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

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

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

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

### What happens on the call

When `reach_person()` is invoked, the agent automatically:

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

If you provided custom `outcomes`, those are used instead.

### Voicemail handling

<Callout>
  <span className="text-primary font-semibold">Warning:</span> <code>reach_person()</code> and <code>set_voicemail_action()</code> both handle voicemail and <strong>cannot be used together</strong>. If you set <code>voicemail_message</code> or <code>voicemail_hangup</code> on <code>reach_person()</code>, do not call <code>set_voicemail_action()</code> — and vice versa. Using both raises an error.
</Callout>

### Common mistake: redundant introductions

<Callout>
  <span className="text-primary font-semibold">Warning:</span> By the time `on_reach_person` fires, the agent has already introduced itself and stated the purpose of the call. Do **not** re-introduce in the first task after `reach_person`.
</Callout>

```python
# 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."),
            ...
        ])
```

<NextLink section="set-voicemail-action" label="set_voicemail_action()" />

---

<!-- section: set-voicemail-action -->

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.

<Callout>
  <span className="text-primary font-semibold">Warning:</span> <code>set_voicemail_action()</code> and <code>reach_person()</code> both handle voicemail and <strong>cannot be used together</strong>. If you are using <code>reach_person()</code>, set voicemail behavior there via its <code>voicemail_message</code> or <code>voicemail_hangup</code> parameters instead. Using both raises an error.
</Callout>

<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" }}
/>

<Callout>
  <span className="text-primary font-semibold">Tip:</span> You must specify exactly one of <code>hangup</code> or <code>message</code> — passing both or neither raises an error.
</Callout>

<NextLink section="read-script" label="read_script()" />

---

<!-- section: read-script -->

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" }}
/>

<Callout>
  <span className="text-primary font-semibold">Note:</span> Unlike `Say` in a checklist, `read_script()` fires before any LLM turn and before any task is set. It's the agent's very first words.
</Callout>

<NextLink section="get-field" label="get_field()" />

---

<!-- section: add-info -->

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" }}
/>

<NextLink section="get-field" label="get_field()" />

---

<!-- section: get-field -->

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"`) |

<Callout>
  <span className="text-primary font-semibold">Tip:</span> You can call `get_field()` at any point after the field has been collected — not just in `on_task_complete`. Use it in mid-call callbacks to personalize subsequent steps.
</Callout>

<NextLink section="intent-recognizer" label="IntentRecognizer" />

---

<!-- section: intent-helpers -->

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)

<Callout>
  <span className="text-primary font-semibold">Deprecated:</span> `IntentRecognizer` from `guava.helpers.openai` is deprecated. Use the new `IntentRecognizer` from `guava.helpers.llm` above instead.
</Callout>

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

<Callout>
  <span className="text-primary font-semibold">Deprecated:</span> `IntentClarifier` from `guava.helpers.openai` is deprecated. Use the new `IntentRecognizer` from `guava.helpers.llm` above instead — it returns all plausible matches by default, replacing the need for a separate clarifier.
</Callout>

`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" }}
/>

<NextLink section="document-qa" label="DocumentQA" />

---

<!-- section: document-qa -->

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. |

<Callout>
  <span className="text-primary font-semibold">namespace requirement:</span> In server mode, `namespace` is required when running multiple `DocumentQA` instances concurrently — even across different files. Without a namespace, concurrent instances may interfere with each other's document stores.
</Callout>

### 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 <a href="/docs/vector-stores">Vector Stores</a> 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.

<CodeBlock code={DOCUMENT_QA_MGMT_EX_PY} filename="document_management.py" language="python" />

<NextLink section="datetime-filter" label="DatetimeFilter" />

---

<!-- section: datetime-filter -->

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

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

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

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

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

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

dt_filter = DatetimeFilter(source_list=AVAILABLE_SLOTS)

matches, suggestions = dt_filter.filter("tomorrow morning", max_results=3)
# matches     == ["2026-04-16T09:00:00", "2026-04-16T10:30:00"]
# suggestions == []  (not needed — matches were found)

matches, suggestions = dt_filter.filter("this Friday at noon", max_results=3)
# matches     == []  (no Friday noon slot exists)
# suggestions == ["2026-04-17T14:00:00", ...]  (nearby alternatives offered)`;

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

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

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

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

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

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

datetime_filter = DatetimeFilter(source_list=AVAILABLE_SLOTS)

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

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

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

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

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

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

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

## DatetimeFilter

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

### Constructor

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

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

### Methods

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

Returns a 2-tuple `(matching_appointments, other_appointments)`:
- `matching_appointments`: datetimes from `source_list` that match `query`, capped at `max_results`.
- `other_appointments`: alternative datetimes to suggest when `matching_appointments` is empty, also capped at `max_results`.

### Edge Cases

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

<Callout>
  <span className="text-primary font-semibold">Model details:</span> `DatetimeFilter` uses `gpt-5-mini` with `reasoning.effort = "medium"`. These settings are not configurable.
</Callout>

### 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" }}
/>

<NextLink section="vector-stores" label="Vector Stores" />

---

<!-- section: vector-stores -->

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.

<Callout>
  <span className="text-primary font-semibold">Python only:</span> Vector store backends are currently available in Python only. TypeScript equivalents are not yet available.
</Callout>

### Installation

Install only the backend(s) you need:

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

Embedding and generation provider extras:

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

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

### ChromaVectorStore

`from guava.helpers.chromadb import ChromaVectorStore`

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

### LanceDBStore

`from guava.helpers.lancedb import LanceDBStore`

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `path` | `str` | No | `"./lancedb_data"` | Local path or GCS URI (e.g. `"gs://bucket/lancedb"`) for storage. |
| `table_name` | `str` | No | `"chunks"` | LanceDB table name. |
| `embedding_model` | `EmbeddingModel` | **Yes** | — | Embedding model to use. Pass a configured instance such as `GenAIEmbedding` or `OpenAIEmbedding`. |

<Callout>
  <span className="text-primary font-semibold">Note:</span> LanceDB silently drops tables that predate the current schema version. This triggers a full re-index the next time `DocumentQA` ingests documents.
</Callout>

### 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.

<Callout>
  <span className="text-primary font-semibold">Managed Postgres:</span> Managed services (Cloud SQL, AlloyDB, RDS) are untested but expected to work since the implementation uses standard `psycopg`.
</Callout>

### 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. |

<Callout>
  <span className="text-primary font-semibold">Cold start:</span> Pinecone index creation can take 30–60 seconds on first use. Subsequent instantiations with the same `index_name` skip creation and connect immediately.
</Callout>

### 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. |

<Callout>
  <span className="text-primary font-semibold">thinking_budget compatibility:</span> The default `thinking_budget=0` works with `gemini-2.5-flash`. If you switch to a non-thinking model like `gemini-1.5-flash`, pass `thinking_budget=None` — that model raises an error when it receives a `thinking_config`.
</Callout>

### 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

<CodeBlock code={VECTOR_STORES_EX_PY} filename="vector_store_examples.py" language="python" />

<NextLink section="inbound-calls" label="Inbound Calls" />

---

<!-- section: api-overview -->

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"
}`}
/>

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

---

<!-- section: conversations-api -->

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

## Conversations

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

<Callout>
  All requests require an `Authorization: Bearer YOUR_GUAVA_API_KEY` header. See the <a href="/docs/api-overview">API Overview</a> for details.
</Callout>

### 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](#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

```bash
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:

```json
{
  "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
}
```

<Callout>
  The `+` in a phone number must be URL-encoded as `%2B` in query strings. The `curl -G --data-urlencode` form above handles this for you.
</Callout>

---

### 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

```bash
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

```bash
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

```bash
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

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

---

<!-- section: messages-api -->

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`:

```json
{
  "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

```bash
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

```bash
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:

```json
{
  "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
}
```

<Callout>
  The `+` in a phone number must be URL-encoded as `%2B` in query strings. The `curl -G --data-urlencode` form above handles this for you.
</Callout>

---

<!-- section: outbound-compliance-setup -->

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](https://app.goguava.ai/dashboard/compliance). 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](https://goguava.ai/terms) 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.

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

---

### 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.

<Callout>
  <span className="text-primary font-semibold">Compliant Language Support:</span> English and Spanish are currently supported for HITRUST / PCI-compliant deployments. All languages other than English and Spanish are not. Support for other languages is planned. See: <a href="/docs/set-language-mode">set_language_mode()</a>
</Callout>

---

### 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](https://app.goguava.ai/dashboard/compliance) 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](https://app.goguava.ai/dashboard/compliance) 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](https://app.goguava.ai/dashboard/compliance) 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](https://app.goguava.ai/dashboard/compliance), 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.

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

#### 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.

<NextLink section="outbound-and-sms-permissions" label="Outbound & SMS Permissions Reference" />

---

<!-- section: webrtc-widgets -->

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"
></script>`}
/>

### 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={`<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>My Voice Agent</title>
  </head>
  <body>
    <script
      src="https://app.goguava.ai/static/build/webrtc-widgets/guava-widget-audio-orb.js"
      data-webrtc-code="grtc-YOUR_AGENT_CODE_HERE"
    ></script>
  </body>
</html>`}
/>

### 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

<div className="my-4 overflow-x-auto rounded-lg border border-border">
  <table className="w-full text-sm font-mono">
    <thead>
      <tr className="border-b border-border bg-card/50">
        <th className="text-left px-4 py-3 text-muted-foreground font-semibold">State</th>
        <th className="text-left px-4 py-3 text-muted-foreground font-semibold">Behavior</th>
        <th className="text-left px-4 py-3 text-muted-foreground font-semibold">User action</th>
      </tr>
    </thead>
    <tbody>
      <tr className="bg-transparent">
        <td className="px-4 py-3 text-primary">idle</td>
        <td className="px-4 py-3 text-muted-foreground">Ready to call — green indicator</td>
        <td className="px-4 py-3 text-muted-foreground">Click to call</td>
      </tr>
      <tr className="bg-card/20">
        <td className="px-4 py-3 text-primary">connecting</td>
        <td className="px-4 py-3 text-muted-foreground">Pulsing animation, clicks disabled</td>
        <td className="px-4 py-3 text-muted-foreground">Wait</td>
      </tr>
      <tr className="bg-transparent">
        <td className="px-4 py-3 text-primary">active</td>
        <td className="px-4 py-3 text-muted-foreground">Call in progress — audio-reactive visuals</td>
        <td className="px-4 py-3 text-muted-foreground">Click to hang up</td>
      </tr>
      <tr className="bg-card/20">
        <td className="px-4 py-3 text-primary">error</td>
        <td className="px-4 py-3 text-muted-foreground">Red indicator — connection failed</td>
        <td className="px-4 py-3 text-muted-foreground">Click to retry</td>
      </tr>
    </tbody>
  </table>
</div>

<Callout>
  <span className="text-primary font-semibold">No configuration needed.</span> The widget is entirely self-contained — it injects its own HTML and CSS (scoped under `[data-guava-widget]` so it won't conflict with your page styles), wrapped in an IIFE with no global variables. Works on any page with a modern browser.
</Callout>

### 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.

<GetStartedCTA />

---

<!-- section: base44-guide -->

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](https://base44.com). 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.

<Prose>
<strong>You'll need</strong> two tools:<br/>
<strong>Base44</strong> for building your web app<br/>
<strong>an AI coding platform with terminal access</strong> (such as <a href="https://claude.ai/code" target="_blank" rel="noopener noreferrer">Claude Code</a> or <a href="https://www.cursor.com" target="_blank" rel="noopener noreferrer">Cursor</a>) 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.)
</Prose>

<Callout>
<div style={{color: "var(--foreground)"}}>
<p style={{margin: "0 0 0.5rem 0"}}><strong>Already have a deployed agent and a <code style={{fontSize: "0.85em"}}>grtc-</code> code?</strong><br/>
Skip to the <a href="#quick-start" className="underline font-semibold">Quick Start</a>.</p>
<p style={{margin: 0}}><strong>New to Guava?</strong><br/>
Start with the <a href="#full-guide" className="underline font-semibold">Full Guide</a>.</p>
</div>
</Callout>

## Quick Start

<Prose>
<strong>You'll need</strong> two items ready in order to follow this Quick Start:
</Prose>

- **a deployed Guava agent**
- **a WebRTC code** (starts with `grtc-`)

<Prose>
If you do not have both of these yet, follow the <a href="#full-guide">Full Guide</a> below.
</Prose>

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

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

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

<script src="https://app.goguava.ai/static/build/webrtc-widgets/guava-widget-audio-orb.js" data-webrtc-code="grtc-YOUR_CODE_HERE"></script>`}
/>

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

#### React component approach

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

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

## Full Guide

#### What is Guava?

<Prose>
<a href="https://goguava.ai" target="_blank" rel="noopener noreferrer">Guava</a> is a software development kit (SDK) for creating AI voice agents. In terms of this guide, Guava is a set of tools and resources that is usable via your preferred AI coding platform, that enables you to add an AI voice agent to your app.
</Prose>

#### What is the voice widget?

<Prose>
The voice widget is a small floating button (called the "orb") that appears on your web app. When a visitor clicks it, their browser opens a live voice conversation with your Guava agent. No phone number or phone call is needed; the conversation happens over Web Real-Time Communication (WebRTC), a technology that enables voice conversations within web apps and pages.
</Prose>

<Prose>
The orb is the default styling of the widget. You can restyle the widget to match your app's design, but that's beyond the scope of this guide.
</Prose>

#### Step 1 — Create a Guava account

<Prose>
Go to <a href="https://app.goguava.ai" target="_blank" rel="noopener noreferrer">app.goguava.ai</a>. Click <strong>Get Started</strong> in the top right, and create your account.
</Prose>

#### Step 2 — Create an API key

<Prose>
An API key lets Guava (via the Guava CLI, which you'll install in the next step) connect to your account.
</Prose>

<Prose>
To create an API key:<br/>
1. In your Guava account, go to the <a href="https://app.goguava.ai/dashboard/api-keys" target="_blank" rel="noopener noreferrer">API Keys</a> page.<br/>
2. Click <strong>+ Create API Key</strong> at the top right.<br/>
3. Name your key something related to your app or voice widget.
</Prose>

<Prose>
Your key will always be available on the <a href="https://app.goguava.ai/dashboard/api-keys" target="_blank" rel="noopener noreferrer">API Keys</a> page, so you can copy-paste it whenever you need. Saving a backup in a password manager is good practice, but not required.
</Prose>

#### Step 3 — Install the Guava CLI

<Prose>
The Guava CLI (Command Line Interface) is the tool that connects your agent code to Guava's platform. Your AI coding platform can operate it for you. You won't need to type terminal commands. (But you could if you want to.)
</Prose>

<Prose>
Paste this prompt into your AI coding platform:
</Prose>

<CodeBlock
  language="text"
  code={`Install the Guava CLI on my machine.`}
/>

<Prose>
Next, connect the CLI to your Guava account. This is a one-time step. Even if you are already signed in to Guava in your browser, the CLI needs its own login. Paste this prompt into your AI coding platform:
</Prose>

<CodeBlock
  language="text"
  code={`Log in to my Guava account using the Guava CLI.`}
/>

<Prose>
A browser tab will open. Sign in with the account you created in Step 1. Once you sign in, the CLI stores your credentials so you won't have to repeat this step.
</Prose>

#### Step 4 — Get your agent starter file

<Prose>
Both options below produce a project folder on your computer with a Python file (<code>{'__main__.py'}</code> or <code>main.py</code>) that defines your agent's behavior and goals. You don't need to know Python: once you have the project folder, you can use your preferred AI coding platform to make changes in plain language.
</Prose>

<Prose>
Choose the option that best fits your situation:
</Prose>

<Prose>
- <strong>Start from scratch</strong> — This creates a blank starter agent that can hold a conversation, but has not yet been given a specific task, role, or use case. This is a good option if you either prefer to create your agent from scratch, or you want to create a voice agent that's significantly different from all preexisting examples.
</Prose>
<Prose>
- <strong>Start from an example</strong> — This provides you with a ready-made agent that already does something similar to what you want. Best if you'd rather modify an existing agent than build one from scratch. This option requires accessing the Guava examples library public repository on GitHub, but the guide will walk you through those steps.
</Prose>

##### Start from scratch

<Prose>
Paste the prompt below into your AI coding platform. It will create a project folder called <code>my-agent</code> with starter code that defines a basic voice agent. This starter agent can hold a spoken conversation, but it hasn't been given a specific task, role, or use case. You'll customize that in Step 7.
</Prose>

<CodeBlock
  language="text"
  code={`Create a new Guava agent project called my-agent.`}
/>

<Prose>
You now have an agent project folder called <code>my-agent</code>. Feel free to rename it to something more personalized, for example: <code>first-webapp-concierge</code>. You'll develop and customize your agent soon. For now, proceed to Step 5.
</Prose>

##### Start from an example

<Prose>
Guava maintains a library of ready-made example agents organized by industry (healthcare, retail, insurance, and more). You can browse the library, download an agent that's close to what you want, and customize it from there.
</Prose>

<Prose>
The examples library is hosted on GitHub, a website where code projects are stored and shared. You don't need a GitHub account, because the Guava example agents library is shared publicly. You'll visit the Guava example library page, browse the available agents, and download the one you want as a ZIP file.
</Prose>

<Callout>
Since your voice widget activates when someone clicks it in your app, you'll want an <strong>inbound</strong> agent, one that responds when a user starts a conversation, rather than an outbound agent that initiates calls. Look for inbound examples when browsing.
</Callout>

<Prose>
<strong>1. Browse the examples.</strong> Go to <a href="https://github.com/goguava-ai/guava-starter" target="_blank" rel="noopener noreferrer">github.com/goguava-ai/guava-starter</a>. Click the <strong>examples</strong> folder to see the list of industries. Click into any industry folder to see the available agents (for example, <code>examples → healthcare → appointment_scheduling</code>). Identify the example agent you'd like to use.
</Prose>

<Prose>
<strong>2. Download the library.</strong> Once you've found an agent you like, go back to the <a href="https://github.com/goguava-ai/guava-starter" target="_blank" rel="noopener noreferrer">main page</a>. Click the green <strong>Code</strong> button near the top right, then click <strong>Download ZIP</strong>. This downloads the entire examples library as a single ZIP file. Find the downloaded file (usually in your Downloads folder) and double-click it to unzip.
</Prose>

<Callout>
GitHub doesn't offer a way to download a single folder, only the entire project. That's why you're downloading the full library. In the next step, you'll copy out just the agent you want.
</Callout>

<Prose>
<strong>3. Create your agent project.</strong> Copy the agent folder you chose (for example, <code>examples/healthcare/appointment_scheduling/</code>) to a location where you keep your projects. Feel free to rename it to something more personalized, for example: <code>first-webapp-concierge</code>. This copy is your working agent project, the one you'll customize, test, and deploy through the rest of this guide.
</Prose>

<Prose>
<strong>4. Connect your AI coding platform.</strong> Open your agent project folder in your AI coding platform. This will be how you develop and customize your agent soon. For now, proceed to Step 5.
</Prose>

#### Step 5 — Create your WebRTC code

<Prose>
A WebRTC code connects the voice widget displayed on your web app to your agent running on Guava's servers. You'll use this code in two places: your agent's code (so it knows to listen for widget connections) and your Base44 app (so the widget knows which agent to connect to).
</Prose>

<Prose>
In your Guava account dashboard, go to the <a href="https://app.goguava.ai/dashboard/webrtc-codes" target="_blank" rel="noopener noreferrer">WebRTC Codes</a> page and click <strong>Create WebRTC Code</strong>. Your new code will appear. Copy it and <strong>save it somewhere</strong>. Unlike the API key from Step 2, this code may not be retrievable later, so make sure you save it now. You'll need it in the next two steps.
</Prose>

<Prose>
<strong>Advanced:</strong> You can also create a WebRTC code using the Guava CLI. See the <a href="/docs/webrtc-widgets">WebRTC Widgets</a> docs for details.
</Prose>

#### Step 6 — Connect your WebRTC code to your agent

<Prose>
Your agent needs to know which WebRTC code to use so that visitors who click the widget on your Base44 app connect to the right agent. You'll use the same code again in Step 9 when you add the widget to your Base44 app.
</Prose>

<Prose>
Open your agent folder in your preferred AI coding platform and paste this prompt, replacing <code>grtc-YOUR_CODE_HERE</code> with the code you created in Step 5:
</Prose>

<CodeBlock
  language="text"
  code={`Update my agent to use the WebRTC code grtc-YOUR_CODE_HERE for voice widget connections.`}
/>

#### Step 7 — Customize and test your agent

<Prose>
Now it's time to design your AI voice agent. Repeat steps 7A and 7B until you're happy with the agent you've created.
</Prose>

##### Step 7A — Customize your agent

<Prose>
Your agent's behavior and goals are defined in the Python file in your project folder. You can change any of this by describing what you want in plain language to your AI coding platform. Refer your AI coding platform to the <a href="/docs/agent">Agent docs</a> for the full list of things you can configure in your agent: persona, tasks, conversation flow, and more.
</Prose>

##### Step 7B — Test your changes

<Prose>
Before deploying your agent, test it on your own computer to make sure it works the way you want. Paste this prompt into your AI coding platform:
</Prose>

<CodeBlock
  language="text"
  code={`Run my Guava agent locally so I can test it.`}
/>

<Prose>
Your AI coding platform will start the agent and provide a link that opens a voice conversation in your browser. Try out the conversation flow, check that it asks the right questions and responds the way you intended. If something isn't right, describe the changes you want to your AI coding platform, then test again. Repeat until you have the AI voice agent you want.
</Prose>

#### Step 8 — Deploy your agent

<Prose>
Deploying sends a copy of your agent to Guava's servers so it can run around the clock and handle real visitors on your website. Your local files stay on your computer; deploying does not remove them.
</Prose>

<Callout>
Deploying won't make your agent appear on your Base44 app yet; it only makes your agent available on Guava's servers. You'll make your agent "go live" in Step 9.
</Callout>

<Prose>
Paste this prompt into your AI coding platform:
</Prose>

<CodeBlock
  language="text"
  code={`Deploy my Guava agent.`}
/>

<Callout>
If you want to take your agent offline later (for example, while you make major changes or if you no longer need it), run <code>guava deploy down ./my-agent</code>. You can redeploy at any time with <code>guava deploy up ./my-agent</code>. Replace <code>my-agent</code> with your project folder name if you renamed it.
</Callout>

#### Step 9 — Add the widget to your Base44 app

<Prose>
At the end of this step, your voice agent will be live on your Base44 app.
</Prose>

<Prose>
(Note: This is the point where the Quick Start begins. If you're resuming work from this step, you can keep reading here, or switch to the <a href="#quick-start" className="underline">Quick Start</a>.)
</Prose>

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

<Prose>
- <strong>Script tag approach</strong> — The simplest option: no code changes, works in every Base44 project.
</Prose>
<Prose>
- <strong>React component approach</strong> — 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.
</Prose>

##### Script tag approach

<Prose>
Paste this prompt into Base44's AI chat. Replace <code>grtc-YOUR_CODE_HERE</code> with the WebRTC code you created in Step 5:
</Prose>

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

<script src="https://app.goguava.ai/static/build/webrtc-widgets/guava-widget-audio-orb.js" data-webrtc-code="grtc-YOUR_CODE_HERE"></script>`}
/>

##### React component approach

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

#### Step 10 — Keep improving (optional)

<Prose>
Your agent is live on your Base44 app. As you get feedback from real conversations, you can keep refining it. From your Guava account, you can monitor your agent's status on the <a href="https://app.goguava.ai/dashboard/deployments" target="_blank" rel="noopener noreferrer">Deployments</a> page and review every conversation on the <a href="https://app.goguava.ai/dashboard/conversations" target="_blank" rel="noopener noreferrer">Conversations</a> page.
</Prose>

- **Adjust what your agent says and does** — make changes in your AI coding platform and redeploy with `guava deploy up ./my-agent` (replace `my-agent` with your project folder name if you renamed it). See the [Agent docs](/docs/agent) for the full API.
- **Add a knowledge base** — let your agent answer questions from your own documents. See [DocumentQA](/docs/document-qa).
- **Customize the widget** — see [WebRTC Widgets](/docs/webrtc-widgets) for widget appearance, states, and advanced configuration.

<details>
<summary><strong>Troubleshooting</strong></summary>

<br />

**I don't see the voice orb.**

<Prose>
Go back to Step 9 and make sure the Base44 chat prompt was applied correctly. In Base44's Code tab, open <code>index.html</code> and confirm the script tag appears just before <code>&lt;/body&gt;</code>. Also check that the <code>grtc-</code> code in the script tag matches the code you created in Step 5.
</Prose>

**The orb shows a red indicator.**

<Prose>
This means the widget cannot connect to your agent. The most common cause is that your agent is no longer deployed. Check its status on the <a href="https://app.goguava.ai/dashboard/deployments" target="_blank" rel="noopener noreferrer">Deployments</a> page in your Guava account. If it shows as inactive, redeploy by following Step 8 again.
</Prose>

**The widget loads but the voice connection fails.**

<Prose>
If the green orb appears but you hear nothing when you click it, your browser may be blocking the voice connection. Open your browser's developer tools (usually F12 or right-click → Inspect) and check the Console tab for error messages. If you see messages mentioning "CSP" or "Content Security Policy," this means Base44's security settings are preventing the voice connection from reaching Guava's servers. Contact <a href="https://base44.com/support" target="_blank" rel="noopener noreferrer">Base44 support</a> to ask about allowing WebRTC connections in your app's security policy.
</Prose>

</details>

<NextLink section="webrtc-widgets" label="WebRTC Widgets Reference" />

---

<!-- section: lovable-guide -->

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

## Add a Guava Voice Agent to Your Lovable App

This guide shows you how to add a Guava voice agent to a web app built with [Lovable](https://lovable.dev). By the end, your Lovable app will have a floating widget that visitors can click to start a spoken conversation with your AI agent. No phone number is required.

<Prose>
<strong>You'll need</strong> two tools:<br/>
<strong>Lovable</strong> for building your web app<br/>
<strong>an AI coding platform with terminal access</strong> (such as <a href="https://claude.ai/code" target="_blank" rel="noopener noreferrer">Claude Code</a> or <a href="https://www.cursor.com" target="_blank" rel="noopener noreferrer">Cursor</a>) 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.)
</Prose>

## Quick Start

<Prose>
<strong>You'll need</strong> two items ready in order to follow this Quick Start:
</Prose>

- **a deployed Guava agent**
- **a WebRTC code** (starts with `grtc-`)

<Prose>
If you do not have both of these yet, follow the <a href="#full-guide">Full Guide</a> below.
</Prose>

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

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

#### Script tag approach

<Prose>
Paste this prompt into Lovable's AI chat. Replace <code>grtc-YOUR_CODE_HERE</code> with your actual WebRTC code before pasting:
</Prose>

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

<script src="https://app.goguava.ai/static/build/webrtc-widgets/guava-widget-audio-orb.js" data-webrtc-code="grtc-YOUR_CODE_HERE"></script>`}
/>

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

#### React component approach

<Prose>
Paste this prompt into Lovable's AI chat. Replace <code>grtc-YOUR_CODE_HERE</code> with your actual WebRTC code before pasting:
</Prose>

## Full Guide

#### What is Guava?

#### What is the voice widget?

#### Step 1 — Create a Guava account

#### Step 2 — Create an API key

<Prose>
An API key lets Guava (via the Guava CLI, which you'll install in the next step) connect to your account.
</Prose>

#### Step 3 — Install the Guava CLI

<Prose>
Paste this prompt into your AI coding platform:
</Prose>

<CodeBlock
  language="text"
  code={`Install the Guava CLI on my machine.`}
/>

<CodeBlock
  language="text"
  code={`Log in to my Guava account using the Guava CLI.`}
/>

#### Step 4 — Get your agent starter file

<Prose>
Choose the option that best fits your situation:
</Prose>

##### Start from scratch

<CodeBlock
  language="text"
  code={`Create a new Guava agent project called my-agent.`}
/>

##### Start from an example

#### Step 5 — Create your WebRTC code

<Prose>
A WebRTC code connects the voice widget displayed on your web app to your agent running on Guava's servers. You'll use this code in two places: your agent's code (so it knows to listen for widget connections) and your Lovable app (so the widget knows which agent to connect to).
</Prose>

#### Step 6 — Connect your WebRTC code to your agent

<Prose>
Your agent needs to know which WebRTC code to use so that visitors who click the widget on your Lovable app connect to the right agent. You'll use the same code again in Step 9 when you add the widget to your Lovable app.
</Prose>

<CodeBlock
  language="text"
  code={`Update my agent to use the WebRTC code grtc-YOUR_CODE_HERE for voice widget connections.`}
/>

#### Step 7 — Customize and test your agent

<Prose>
Now it's time to design your AI voice agent. Repeat steps 7A and 7B until you're happy with the agent you've created.
</Prose>

##### Step 7A — Customize your agent

##### Step 7B — Test your changes

<CodeBlock
  language="text"
  code={`Run my Guava agent locally so I can test it.`}
/>

#### Step 8 — Deploy your agent

<Callout>
Deploying won't make your agent appear on your Lovable app yet; it only makes your agent available on Guava's servers. You'll make your agent "go live" in Step 9.
</Callout>

<Prose>
Paste this prompt into your AI coding platform:
</Prose>

<CodeBlock
  language="text"
  code={`Deploy my Guava agent.`}
/>

#### Step 9 — Add the widget to your Lovable app

<Prose>
At the end of this step, your voice agent will be live on your Lovable app.
</Prose>

<Prose>
There are two ways to add the widget to your Lovable app. Choose the one that suits your preference:
</Prose>

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

##### Script tag approach

<Prose>
Paste this prompt into Lovable's AI chat. Replace <code>grtc-YOUR_CODE_HERE</code> with the WebRTC code you created in Step 5:
</Prose>

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

<script src="https://app.goguava.ai/static/build/webrtc-widgets/guava-widget-audio-orb.js" data-webrtc-code="grtc-YOUR_CODE_HERE"></script>`}
/>

##### React component approach

<Prose>
Paste this prompt into Lovable's AI chat. Replace <code>grtc-YOUR_CODE_HERE</code> with your actual WebRTC code before pasting:
</Prose>

#### Step 10 — Keep improving (optional)

<Prose>
Your agent is live on your Lovable app. As you get feedback from real conversations, you can keep refining it. From your Guava account, you can monitor your agent's status on the <a href="https://app.goguava.ai/dashboard/deployments" target="_blank" rel="noopener noreferrer">Deployments</a> page and review every conversation on the <a href="https://app.goguava.ai/dashboard/conversations" target="_blank" rel="noopener noreferrer">Conversations</a> page.
</Prose>

<details>
<summary><strong>Troubleshooting</strong></summary>

<br />

**I don't see the voice orb.**

<Prose>
Go back to Step 9 and make sure the Lovable chat prompt was applied correctly. In Lovable's Code Mode, open <code>index.html</code> and confirm the script tag appears just before <code>&lt;/body&gt;</code>. Also check that the <code>grtc-</code> code in the script tag matches the code you created in Step 5.
</Prose>

**The orb shows a red indicator.**

</details>

<NextLink section="webrtc-widgets" label="WebRTC Widgets Reference" />

---

<!-- section: agent-testing -->

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

## Automated Testing for Guava Agents

<LanguageTabs
  pythonContent={<><Callout>This guide focuses on automated testing. For manual testing, use <code>agent.call_local()</code> to place a test call using your local audio device, or <code>agent.chat()</code> to interact with your agent via text in the terminal.</Callout><Prose>Guava Agent tests are defined in code. You can write them using your existing testing framework, such as <code>pytest</code> or Python's built-in <code>unittest</code> module. This lets you test your Agent end-to-end alongside your Expert's code, using patterns you're already familiar with.</Prose></>}
  typescriptContent={<><Callout>This guide focuses on automated testing. For manual testing, use <code>agent.callLocal()</code> to place a test call using your local audio device, or <code>agent.chat()</code> to interact with your agent via text in the terminal.</Callout><Prose>Guava Agent tests are defined in code. You can write them using your existing testing framework, such as Jest or Vitest. This lets you test your Agent end-to-end alongside your Expert's code, using patterns you're already familiar with.</Prose></>}
/>

### Test individual Agent handlers with `MockCall`

export const MOCK_CALL_EX_PY = `import unittest
from guava.testing import MockCall

# Import a handler from the help desk example.
from guava.examples.help_desk import on_action_request

class TestHandlers(unittest.TestCase):
    def test_routes_to_sales(self):
        # We can directly invoke that handler with a mocked Call object.
        actions = on_action_request(MockCall(), "I want to buy a new sofa")
        self.assertEqual("sales", actions[0].key)`;

export const MOCK_CALL_EX_TS = `import { MockCall } from "@guava-ai/guava-sdk";

// Import the agent from the help-desk example.
import { agent } from "@guava-ai/guava-sdk/examples/help-desk";

test("routes to sales", async () => {
    // We can directly invoke that handler with a mocked Call object.
    const suggestion = await agent.handlers.onActionRequest(new MockCall(), "I want to buy a new sofa");
    expect(suggestion).toHaveProperty("key", "sales");
});`;

<LanguageTabs
  pythonContent={<>
    <Prose>The most basic way to test Guava Agents is to test individual handlers like <code>on_action_request</code> or <code>on_question</code>. You can do so by constructing a <code>guava.testing.MockCall</code> object and passing it in directly to the handler.</Prose>
    <CodeBlock code={MOCK_CALL_EX_PY} filename="test_agent.py" language="python" />
  </>}
  typescriptContent={<>
    <Prose>The most basic way to test Guava Agents is to test individual handlers like <code>onActionRequest</code> or <code>onQuestion</code>. You can do so by constructing a <code>MockCall</code> object and passing it in directly to the handler via <code>agent.handlers</code>.</Prose>
    <CodeBlock code={MOCK_CALL_EX_TS} filename="agent.test.ts" language="typescript" />
  </>}
/>

### Run a roleplay session and analyze the result

export const ROLEPLAY_EX_PY = `import unittest

# Import the agent from the help desk example.
from guava.examples.help_desk import agent

class TestHelpDeskAgent(unittest.TestCase):
    def test_purchase_roleplay(self):
        session = agent.roleplay("You are a caller who wants to buy a new dining table.")

# Use session.get_transcript() to retrieve the session's transcript.
        print(session.get_transcript())

# Use session.evaluate() to assess the Agent's performance against a rubric.
        session.evaluate(
            # These criteria must all be true.
            pass_criteria=["The agent identified itself as working for Clearfield Home & Living."],
            # These criteria must all be false.
            fail_criteria=["The agent directly offered to search the inventory."],
        )

# You can assert some values directly on the session object.
        self.assertIn("sales", session.executed_actions)
        self.assertEqual("bot-transfer", session.termination_reason)`;

export const ROLEPLAY_EX_TS = `import { agent } from "@guava-ai/guava-sdk/examples/help-desk";

test("purchase roleplay", async () => {
    const session = await agent.roleplay("You are a caller who wants to buy a new dining table.");

// Use session.getTranscript() to retrieve the session's transcript.
    console.log(session.getTranscript());

// Use session.evaluate() to assess the Agent's performance against a rubric.
    await session.evaluate({
        // These criteria must all be true.
        passCriteria: ["The agent identified itself as working for Clearfield Home & Living."],
        // These criteria must all be false.
        failCriteria: ["The agent directly offered to search the inventory."],
    });

// You can assert some values directly on the session object.
    expect(session.executedActions).toContain("sales");
    expect(session.terminationReason).toBe("bot-transfer");
});`;

<LanguageTabs
  pythonContent={<>
    <Prose><code>agent.roleplay(prompt="...")</code> runs your agent against a separate LLM that improvises a conversation based on a prompt. The session runs to completion and returns a <code>TestSession</code> object you can assert against. As in a real call, your registered handlers will be invoked by the Agent.</Prose>
    <Prose>The returned session object contains the transcript, useful helpers that can be asserted against, and a <code>session.evaluate</code> function that evaluates the conversation against a rubric.</Prose>
    <CodeBlock code={ROLEPLAY_EX_PY} filename="test_roleplay.py" language="python" />
  </>}
  typescriptContent={<>
    <Prose><code>agent.roleplay(prompt)</code> runs your agent against a separate LLM that improvises a conversation based on a prompt. The session runs to completion and returns a <code>TestSession</code> object you can assert against. As in a real call, your registered handlers will be invoked by the Agent.</Prose>
    <Prose>The returned session object contains the transcript, useful helpers that can be asserted against, and a <code>session.evaluate()</code> function that evaluates the conversation against a rubric.</Prose>
    <CodeBlock code={ROLEPLAY_EX_TS} filename="roleplay.test.ts" language="typescript" />
  </>}
/>

### Patch Agent handlers before testing

In some cases, you may not want every handler to run during a test.
Use `agent.patch()` to create a clone of the Agent where you can override callbacks without modifying the original Agent.

export const PATCH_EX_PY = `import unittest
import guava

# Import the agent from the help desk example.
from guava.examples.help_desk import agent

class TestHelpDeskAgent(unittest.TestCase):
    def test_sales_closed(self):
        # Create a clone of the agent and patch the on_action handler.
        patched = agent.patch()

@patched.on_action("sales")
        def patched_sales(call: guava.Call):
            call.hangup(
                "Tell the caller that the sales department is closed and that "
                "they should call back tomorrow between 9am and 5pm."
            )

# Run the roleplay test with our patched agent.
        session = patched.roleplay("You are a caller who wants to buy a new dining table.")
        session.evaluate(["The agent informed the caller of the business hours from 9am to 5pm."])`;

export const PATCH_EX_TS = `import type { Call } from "@guava-ai/guava-sdk";
import { agent } from "@guava-ai/guava-sdk/examples/help-desk";

test("sales closed", async () => {
    // Create a clone of the agent and patch the onAction handler.
    const patched = agent.patch();

patched.onAction("sales", async (call: Call) => {
        call.hangup(
            "Tell the caller that the sales department is closed and that " +
            "they should call back tomorrow between 9am and 5pm."
        );
    });

// Run the roleplay test with our patched agent.
    const session = await patched.roleplay("You are a caller who wants to buy a new dining table.");
    await session.evaluate({
        passCriteria: ["The agent informed the caller of the business hours from 9am to 5pm."],
    });
});`;

<CodeTabs
  python={{ code: PATCH_EX_PY, filename: "test_patched_agent.py" }}
  typescript={{ code: PATCH_EX_TS, filename: "patched_agent.test.ts" }}
/>

<LanguageTabs
  pythonContent={<Prose>For any function that isn't an Agent handler, you can patch it using <a href="https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch">Python's builtin patching system</a>.</Prose>}
  typescriptContent={<Prose>For any function that isn't an Agent handler, you can patch it using your framework's built-in mocking tools.</Prose>}
/>

### Run a session with full control

export const SCRIPTED_SESSION_EX_PY = `import unittest

# Import the agent from the help desk example.
from guava.examples.help_desk import agent

class TestHelpDeskAgent(unittest.TestCase):
    def test_purchase_routes_to_sales(self):
        with agent.test() as session:
            # Wait until the agent has finished its opening turn.
            session.wait_for_turn()

# Inject a caller utterance and let the agent respond.
            session.say("Hi, I'm looking to make a new purchase.")

# Wait until the session ends (transfer, hangup, etc.).
            session.wait_for_end()

# The TestSession here is the same type as those returned from roleplay sessions.
        # Assert against any of its values, read the transcript, or use 'session.evaluate(...)'
        self.assertIn("sales", session.executed_actions)
        self.assertEqual("bot-transfer", session.termination_reason)`;

export const SCRIPTED_SESSION_EX_TS = `import { agent } from "@guava-ai/guava-sdk/examples/help-desk";

test("purchase routes to sales", async () => {
    const session = await agent.test(async (session) => {
        // Wait until the agent has finished its opening turn.
        await session.waitForTurn();

// Inject a caller utterance and let the agent respond.
        session.say("Hi, I'm looking to make a new purchase.");

// Wait until the session ends (transfer, hangup, etc.).
        await session.waitForEnd();
    });

// The TestSession here is the same type as those returned from roleplay sessions.
    // Assert against any of its values, read the transcript, or use 'session.evaluate(...)'
    expect(session.executedActions).toContain("sales");
    expect(session.terminationReason).toBe("bot-transfer");
});`;

`agent.test()` gives you a live test session where you have full control over timing and caller utterances.
Use this when you need precise control over how the conversation unfolds.

<CodeTabs
  python={{ code: SCRIPTED_SESSION_EX_PY, filename: "test_help_desk.py" }}
  typescript={{ code: SCRIPTED_SESSION_EX_TS, filename: "help_desk.test.ts" }}
/>

---

<!-- section: campaign -->

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

## Outbound Campaigns

While you can use <LanguageAlternate pythonContent={<code>agent.call_phone("+1...")</code>} typescriptContent={<code>agent.callPhone("+1...")</code>} /> to place singular outbound calls, we recommend using Campaigns when calling multiple contacts.

Campaigns automatically manage missed-call retries, enforce calling windows, and control call concurrency.
Since they are persistent resources, you should create one Campaign for each distinct use case and continue adding contacts to it as needed.

While Campaigns are responsible for dispatching calls - you will still need to "attach" an Agent to handle the call itself - this is done using a unique "code" created for each Campaign.

## Before you start

You'll need:

- **A Guava account** — sign up at [app.goguava.ai](https://app.goguava.ai)
- **A phone number** — from the [Phone Numbers](https://app.goguava.ai/dashboard/phone-numbers) page
- **Outbound permission** — outbound dialing requires approval. Fill out the **Outbound Dialing Permissions Request** form on the [Compliance](https://app.goguava.ai/dashboard/compliance) page.

## Create a campaign in the dashboard

Open the [Campaigns](https://app.goguava.ai/dashboard/campaigns) page and click **Create Campaign**. Configure your campaign name, origin phone numbers, calling windows, and retry settings.

Once created, take note of the campaign code — it starts with `gcmp-...`.

## Upload contacts

From the campaign's detail page in the dashboard, upload your contact list. Each contact needs a phone number and any per-call data variables your agent will use (such as a patient name or appointment time).

These variables will be accessible inside your agent callbacks via <LanguageAlternate pythonContent={<code>call.get_variable()</code>} typescriptContent={<code>call.getVariable()</code>} />.

## Write your agent

Define an `Agent` and attach callbacks for each stage of the call.

export const AGENT_PY = `import guava
from guava import Agent, Field

agent = Agent(
    name="Sarah",
    organization="Valley Dental",
    purpose="Remind patients about their upcoming dental appointments.",
)

# Fires at the start of every call. We'll start by using reach_person()
# to confirm we're talking to the right person.
@agent.on_call_start
def on_call_start(call: guava.Call):
    call.reach_person(
        contact_full_name=call.get_variable("patient_name"),
    )

# Fires once reach_person() resolves. outcome="available" means the contact answered.
@agent.on_reach_person
def on_reach_person(call: guava.Call, outcome: str):
    if outcome == "available":
        # Appointment details are only loaded into the context after the
        # caller's identity has been confirmed.
        call.add_info("appointment_details", {
            "patient_name": call.get_variable("patient_name"),
            "appointment_date": call.get_variable("appointment_date"),
            "appointment_time": call.get_variable("appointment_time")
        })

call.set_task(
            "confirm_appointment",
            objective="Confirm the appointment date and time.",
            checklist=[
                Field(
                    key="appointment_response",
                    description="Whether the patient confirms, reschedules, or cancels",
                    field_type="multiple_choice",
                    choices=["confirmed", "reschedule", "cancel"],
                ),
            ],
        )
    else:
        call.hangup()

# Fires when all checklist items are resolved.
@agent.on_task_complete("confirm_appointment")
def on_confirmed(call: guava.Call):
    call.hangup("Thank them and end the call.")`;

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

const agent = new guava.Agent({
  name: "Sarah",
  organization: "Valley Dental",
  purpose: "Remind patients about their upcoming dental appointments.",
});

// Fires at the start of every call. We'll start by using reachPerson()
// to confirm we're talking to the right person.
agent.onCallStart(async (call: guava.Call) => {
  await call.reachPerson(await call.getVariable("patient_name"));
});

// Fires once reachPerson() resolves. outcome="available" means the contact answered.
agent.onReachPerson(async (call: guava.Call, outcome: string) => {
  if (outcome === "available") {
    // Appointment details are only loaded into the context after the
    // caller's identity has been confirmed.
    await call.addInfo("appointment_details", {
      patient_name: await call.getVariable("patient_name"),
      appointment_date: await call.getVariable("appointment_date"),
      appointment_time: await call.getVariable("appointment_time"),
    });

await call.setTask({
      taskId: "confirm_appointment",
      objective: "Confirm the appointment date and time.",
      checklist: [
        guava.Field({
          key: "appointment_response",
          description: "Whether the patient confirms, reschedules, or cancels",
          fieldType: "multiple_choice",
          choices: ["confirmed", "reschedule", "cancel"],
        }),
      ],
    });
  } else {
    await call.hangup();
  }
});

// Fires when all checklist items are resolved.
agent.onTaskComplete("confirm_appointment", async (call) => {
  await call.hangup("Thank them and end the call.");
});`;

<LanguageTabs
  pythonContent={<CodeBlock code={AGENT_PY} filename="campaign.py" language="python" />}
  typescriptContent={<CodeBlock code={AGENT_TS} filename="campaign.ts" language="typescript" />}
/>

export const TEST_PY = `agent.chat(variables={
    "patient_name": "Jane Smith",
    "appointment_date": "Monday, June 23rd",
    "appointment_time": "2:00 PM",
})`;

export const TEST_TS = `await agent.chat({
  patient_name: "Jane Smith",
  appointment_date: "Monday, June 23rd",
  appointment_time: "2:00 PM",
});`;

## Test your agent

<LanguageTabs
  pythonContent={<><Prose>Before attaching to a campaign, test your agent using <code>agent.call_local()</code> or <code>agent.chat()</code>. Pass a <code>variables</code> dict to simulate per-contact data:</Prose><CodeBlock code={TEST_PY} filename="campaign.py" language="python" /></>}
  typescriptContent={<><Prose>Before attaching to a campaign, test your agent using <code>agent.callLocal()</code> or <code>agent.chat()</code>. Pass a <code>variables</code> object to simulate per-contact data:</Prose><CodeBlock code={TEST_TS} filename="campaign.ts" language="typescript" /></>}
/>

export const ATTACH_PY = `agent.attach_campaign(campaign_code="gcmp-...")`;

export const ATTACH_TS = `await agent.attachCampaign("gcmp-...");`;

## Attach and serve the campaign

<LanguageTabs
  pythonContent={<><Prose>Next, run <code>agent.attach_campaign("gcmp-...")</code>. This "attaches" your agent to the Campaign that we previously created.</Prose><CodeBlock code={ATTACH_PY} filename="campaign.py" language="python" /></>}
  typescriptContent={<><Prose>Next, call <code>await agent.attachCampaign("gcmp-...")</code>. This "attaches" your agent to the Campaign that we previously created.</Prose><CodeBlock code={ATTACH_TS} filename="campaign.ts" language="typescript" /></>}
/>

## Review calls in the dashboard

Once your campaign is running, visit the [Campaigns](https://app.goguava.ai/dashboard/campaigns) page to monitor progress.

---

<!-- section: agentic-tenacity -->

import { CodeBlock } from '../views/docs/CodeBlock';
import { Callout, Prose, NextLink } from '../views/docs/prose';
export const AGENTIC_TENACITY_EX = `import os

import guava
from guava import Field, Say
from guava.campaigns import create_or_update_campaign, Contact
from guava.types import OutreachModality

# 1. Create a campaign with a description (required for Agentic Tenacity)
campaign = create_or_update_campaign(
    "political-poll-q2-2026",
    origin_phone_numbers=[os.environ["GUAVA_AGENT_NUMBER"]],
    calling_windows=[
        {"day": day, "start_time": "09:00", "end_time": "17:00"}
        for day in ["monday", "tuesday", "wednesday", "thursday", "friday"]
    ],
    start_date="2026-04-01",
    max_concurrency=3,
    max_attempts=2,
    description="Non-partisan political opinion poll for Q2 2026",
)

# 2. Upload contacts with outreach modalities
campaign.upload_contacts(
    [
        Contact(
            phone_number="+15551234567",
            data={"first_name": "Alice", "district": "District 5"},
            outreach_modalities=["sms"],
        ),
        Contact(
            phone_number="+15559876543",
            data={"first_name": "Bob", "district": "District 12"},
            outreach_modalities=["sms"],
        ),
    ],
    accepted_terms_of_service=True,
)

# Or apply outreach modalities globally to all contacts:
campaign.upload_contacts(
    [
        Contact(phone_number="+15551234567", data={"first_name": "Alice"}),
        Contact(phone_number="+15559876543", data={"first_name": "Bob"}),
    ],
    accepted_terms_of_service=True,
    outreach_modalities=["sms"],  # applied to all contacts
)`;

## Agentic Tenacity

Agentic Tenacity is an extension to Guavadialer that intelligently reaches out to contacts across multiple channels before calling them. Rather than placing blind cold calls, the system warms contacts with pre-call alerts and responds to inbound messages to reschedule, answer questions, or mark contacts as do-not-call.

<Callout>
  <span className="text-primary font-semibold">Current status:</span> SMS is the only supported outreach modality. Email, WhatsApp, and RCS/iMessage support is planned. Only pre-call messages are supported — post-call follow-ups are not yet implemented.
</Callout>

### How it works

When you upload contacts with outreach modalities enabled, Guava sends a pre-call message before each call attempt. For example: "Hi! This is a message from Harper Valley. We are doing a political opinion survey, and you should receive a call from us in the next 10–30 minutes. If that is not a good time, please reply with a time that works better for you."

The system automatically handles inbound replies — rescheduling calls, answering general questions about the campaign, or marking contacts as DNC.

### Setup

<Prose><span className="text-foreground">1. Apply for SMS permissions</span> — contact the Guava team to enable SMS for your account. This is currently a manual process via a Google Form.</Prose>

<Prose><span className="text-foreground">2. Provide a campaign description</span> — the `description` parameter in `create_or_update_campaign()` is required when using Agentic Tenacity. The agent uses it to respond to messages about who is calling and what the campaign is about.</Prose>

### Setting outreach modalities

Modalities can be set in two places. Per-contact settings take priority over global settings.

<Prose><span className="text-foreground">Per contact:</span> Add an `outreach_modalities` field to individual contact objects. This controls which modalities are used for that specific contact.</Prose>

<Prose><span className="text-foreground">Global shortcut:</span> Pass `outreach_modalities` to `campaign.upload_contacts()` to apply the same modalities to all contacts in that batch (unless a contact has its own setting).</Prose>

If neither is set, no pre-call messages are sent and the campaign behaves as a standard Guavadialer campaign.

### Example

<CodeBlock code={AGENTIC_TENACITY_EX} filename="agentic_campaign.py" language="python" />

<NextLink section="deployment" label="Deployment" />

---

<!-- section: deployment -->

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

## Deployment

Guava Deploy is a managed cloud platform that lets you deploy Guava voice-agent projects without provisioning or managing your own infrastructure. When you run `guava deploy up`, the CLI packages your project, builds it in the cloud, and launches it for you. No servers to set up, no infrastructure to manage.

### Security

Your deployments are secure by default:

<Callout>
  <ul className="space-y-2 list-none pl-0 mb-0">
    <li><span className="text-primary font-semibold">Isolated environments</span> — Each deployment runs in its own private sandbox, completely separated from other users' workloads.</li>
    <li><span className="text-primary font-semibold">Network protection</span> — Your sandbox can make outbound requests to the internet (e.g. calling APIs), but no one can connect into your sandbox from the outside.</li>
    <li><span className="text-primary font-semibold">Secure credentials</span> — Your API key and phone number are injected securely at runtime and never appear in logs.</li>
    <li><span className="text-primary font-semibold">Dedicated resources</span> — CPU and memory are reserved for your deployment, so performance is consistent.</li>
  </ul>
</Callout>

### Step-by-step guide

#### Prerequisites

<Prose>- A Guava account</Prose>
<Prose>- A terminal (macOS, Linux, or WSL on Windows)</Prose>

#### Step 1 — Install the CLI

Follow the [Quickstart guide](/docs/quickstart) to install the Guava CLI.

#### Step 2 — Log in

<CodeBlock code="guava login" filename="terminal" language="bash" />

This opens your browser for authentication. Once you log in, the CLI is authenticated and all subsequent commands will use your account.

#### Step 3 — Create a project

<CodeBlock code="guava create my-agent" filename="terminal" language="bash" />

The CLI walks you through interactive configuration:

<Prose>1. **Base image** — Python version (3.10, 3.11, 3.12, 3.13, or 3.14)</Prose>
<Prose>2. **Instance tier** — choose based on your workload:</Prose>

| Tier | CPU | Memory | Use case |
|------|-----|--------|----------|
| `guava-seed` | 1 core | 1Gi | Development / testing |
| `guava-fruit` | 2 cores | 2Gi | Standard production |
| `guava-tree` | 4 cores | 4Gi | High-performance workloads |

<Prose>3. **Phone number** — optionally buy a number now (or later with `guava numbers buy`)</Prose>

This generates the following project structure:

<CodeBlock
  filename="terminal"
  language="bash"
  code={`my-agent/
  .guava          # Project config (project ID, tier, base image, etc.)
  main.py         # Required entry point — your agent code goes here
  pyproject.toml  # Python dependencies
  PRD.md          # Product requirements template
  README.md       # Project readme`}
/>

<Callout>
  <span className="text-primary font-semibold">Important:</span> If you use git, the `.guava` file must be committed and kept up to date. The CLI reads it to identify your project and track deployment state.
</Callout>

#### Deploying an existing project

If you already have a Python project with a `main.py`, you don't need to run `guava create`. Just navigate to your project directory and run:

<CodeBlock code="guava deploy up" filename="terminal" language="bash" />

The CLI will detect that there's no `.guava` config and ask if you'd like to initialize one. It will then prompt you for a base image and instance tier, generate a `.guava` file, and proceed with the deploy.

#### Step 4 — Write your agent code

Edit `main.py` with your voice-agent logic.

For dependencies, Guava supports several common Python workflows. The build system automatically detects which one you're using based on the files in your project:

| Files present | What happens |
|---------------|--------------|
| `uv.lock` + `pyproject.toml` | Installs with `uv sync --frozen` (locked, reproducible) |
| `poetry.lock` + `pyproject.toml` | Installs with `uv sync` |
| `pyproject.toml` (alone) | Installs with `uv sync` |
| `requirements.txt` | Installs with `uv pip install -r requirements.txt` |
| `requirements.in` | Installs with `uv pip install -r requirements.in` |

#### Step 5 — Deploy

<CodeBlock code="guava deploy up" filename="terminal" language="bash" />

The CLI will:

<Prose>1. **Check for changes** — if your code hasn't changed since the last deploy, the build step is skipped automatically.</Prose>
<Prose>2. **Upload your code** to cloud storage.</Prose>
<Prose>3. **Build a container image** with your chosen Python version and dependencies. The CLI shows build progress in the terminal.</Prose>
<Prose>4. **Launch your sandbox** and wait until it's running. The CLI shows the deployment status as it starts up.</Prose>

To force a full rebuild even if your code hasn't changed:

<CodeBlock code="guava deploy up --rebuild" filename="terminal" language="bash" />

If a deployment is already running, the CLI will ask whether to reuse or replace it.

#### Step 6 — Check deployment status

<CodeBlock code="guava deploy status" filename="terminal" language="bash" />

Shows whether your deployment is starting up, running, or has encountered an error.

#### Step 7 — View logs

<CodeBlock
  filename="terminal"
  language="bash"
  code={`# Runtime logs (default: last 200 lines, max 1000)
guava deploy logs
guava deploy logs -n 500

# Build logs (returns a temporary URL to view full build output)
guava deploy build-logs`}
/>

#### Step 8 — List all deployments

<CodeBlock code="guava deploy ls" filename="terminal" language="bash" />

Prints a table with columns: NAME, NUMBER, ACTIVE, ID.

#### Step 9 — Update project configuration

<CodeBlock code="guava deploy update" filename="terminal" language="bash" />

Re-prompts for configuration fields (name, base image, tier) with current values shown as defaults.

#### Step 10 — Check for code changes

<CodeBlock code="guava deploy changed" filename="terminal" language="bash" />

Tells you whether your code has changed since the last deploy.

#### Step 11 — Tear down

<CodeBlock code="guava deploy down" filename="terminal" language="bash" />

Stops the running sandbox. You can also target a specific task:

<CodeBlock code={`guava deploy down --id <task-id>`} filename="terminal" language="bash" />

### Phone number management

Buy a phone number for your project at any time:

<CodeBlock code="guava numbers buy" filename="terminal" language="bash" />

The CLI fetches available numbers, shows you a match, and stores the purchased number in `.guava`. On the next deploy, the number is passed to your sandbox as the `GUAVA_AGENT_NUMBER` environment variable.

### File caching

<Callout>
  If you need to cache a file at runtime, write it to <code>/tmp</code>. Note that <code>/tmp</code> is ephemeral: contents are lost when the sandbox restarts.
</Callout>

### Quick reference

For a full list of commands and options, see the [CLI Reference](/docs/cli-reference).

<NextLink section="cli-reference" label="CLI Reference" />

---

<!-- section: cli-reference -->

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

## CLI Reference

`guava` is the primary interface for working with Guava. It handles authentication, project scaffolding, local runs, deployment, phone number provisioning, conversation history, org management, outbound campaigns, and WebRTC widget codes. The Guava SDK is installed automatically as part of the project template.

### Quick reference

| Command | Description |
|---------|-------------|
| `guava login [--no-launch-browser]` | Authenticate via OAuth (browser-based) |
| `guava logout` | Clear stored credentials |
| `guava create [name] [flags…]` | Scaffold a new project (see `guava create` below) |
| `guava run [target] [-- args…]` | Run the agent locally with `uv` |
| `guava update [flags…]` | Update project configuration (replaces `deploy update`) |
| `guava self-upgrade` | Upgrade the `guava` CLI to the latest version |
| `guava deploy up [dir] [--rebuild]` | Build and deploy |
| `guava deploy down [target] [--id ID]` | Stop a deployment |
| `guava deploy scale <replicas> [dir]` | Scale a running deployment |
| `guava deploy status [target]` | Check deployment state |
| `guava deploy logs [target] [-n N]` | Runtime logs (default 200, max 1000) |
| `guava deploy build-logs [target] [--id ID]` | Get build log URL |
| `guava deploy list` (alias `ls`) | List all deployments |
| `guava deploy changed [dir]` | Check whether code changed since last deploy |
| `guava numbers list` (alias `ls`) | List owned phone numbers |
| `guava numbers buy [dir]` | Provision a phone number for this project |
| `guava conversations list [filters…]` | List conversations (calls) |
| `guava conversations get <call_id>` | Show conversation details |
| `guava conversations transcript <call_id>` | Show conversation transcript |
| `guava conversations recording <call_id> [-o path]` | Download recording (WAV) |
| `guava org` / `guava org list` | List organizations you belong to |
| `guava org use [org_id]` | Switch active org (interactive if omitted) |
| `guava campaigns list` (alias `ls`) | List outbound campaigns |
| `guava campaigns show <code>` | Campaign details and status counts |
| `guava campaigns pause <code>` / `resume <code>` / `delete <code>` | Lifecycle controls |
| `guava campaigns add-contacts <code> <file> [flags…]` | Upload contacts |
| `guava widget [--ttl SEC]` | Mint a WebRTC widget code |

### Command reference

#### `guava login`

Authenticates the CLI against Guava via OAuth. Opens your default browser to a sign-in page and stores the resulting credentials locally so subsequent commands can call the API on your behalf.

| Name | Type | Required | Description |
|------|------|----------|-------------|
| --no-launch-browser | flag | No | Print the login URL instead of opening a browser. |

#### `guava logout`

Clears stored credentials. No parameters.

#### `guava create`

Scaffolds a new Guava project on disk — generates `main.py`, `pyproject.toml`, `PRD.md`, `README.md`, and the `guava.toml` config — and (unless `--skip-deps` is set) runs `uv sync` to install dependencies. For outbound projects it also creates the matching campaign on the server.

`guava create` runs in **interactive mode** by default — the CLI prompts for anything you don't pass. In **non-interactive mode** (when stdin is not a TTY, or when you pass `--no-prompt`), there are no prompts: required flags must be supplied or the command errors, and optional flags fall back to documented defaults.

**Positional argument**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| name | string | No | Project path or name. Defaults to current directory. |

**Non-interactive mode flags** — in interactive mode you can omit any of these and the CLI will prompt; in non-interactive mode they behave as noted under "Required".

| Name | Type | Required (non-interactive) | Description |
|------|------|----------------------------|-------------|
| --no-prompt | flag | No | Force non-interactive mode. Auto-enabled when stdin is not a TTY. |
| --direction | inbound \| outbound | Yes | Call direction for the project. The only flag that has no default. |
| --base-image | string | No | Sandbox base image. Default `python-sandbox:3.14`. |
| --tier | seed \| fruit \| tree | No | Instance tier. Default `seed`. |
| --replicas | integer | No | Number of pod replicas. Default 1. |

Inbound-only (`--direction inbound`):

| Name | Type | Required (non-interactive) | Description |
|------|------|----------------------------|-------------|
| --phone | string | No | Phone source: E.164 number, `any` (first owned number), or `buy` (provision a new one). |

Outbound-only (`--direction outbound`):

| Name | Type | Required (non-interactive) | Description |
|------|------|----------------------------|-------------|
| --calling-windows | JSON | Yes | E.g. `[{"day":"monday","start":"09:00","end":"17:00"}]`. No default — must be supplied. |
| --campaign-name | string | No | Campaign name. Defaults to the project name. |
| --timezone | IANA TZ | No | Default detected from system. |
| --origin-number | string (repeatable) | No | E.164, `any`, or `buy`. Repeat for multiple. |
| --max-concurrency | integer | No | Max concurrent calls. Default 1. |
| --max-attempts | integer | No | Max attempts per recipient. Default 1. |
| --start-date | YYYY-MM-DD | No | Campaign start date. Default today. |
| --description | string | No | Free-form description used as outreach context. |
| --test-phone | E.164 | No | Number that will receive a test call. |

<Callout>
  Passing an inbound-only flag with `--direction outbound` (or vice-versa) will result in an error.
</Callout>

#### `guava run`

Runs the agent locally on your machine. Anything after `--` is forwarded straight to `main.py`, which is useful for one-off flags your agent reads from `argv`.

| Name | Type | Required | Description |
|------|------|----------|-------------|
| target | string | No | Project directory. Defaults to current directory. |
| -- args… | passthrough | No | Anything after `--` is forwarded verbatim to `uv run main.py`. |

#### `guava update`

Edits the fields in `guava.toml` — project name, base image, and instance tier — without re-scaffolding the project. Use this whenever you want to bump the Python sandbox version or move between tiers.

Like `create`, `guava update` runs in **interactive mode** by default and prompts for each field, showing the current value as the default. In **non-interactive mode** (`--no-prompt` or no TTY), no prompts run and any field you don't pass simply keeps its current value — nothing is required.

| Name | Type | Required | Description |
|------|------|----------|-------------|
| target | string | No | Project directory. Defaults to current directory. |

**Non-interactive mode flags** — in interactive mode the CLI prompts for each of these; in non-interactive mode, any flag you omit leaves the existing value unchanged.

| Name | Type | Required (non-interactive) | Description |
|------|------|----------------------------|-------------|
| --no-prompt | flag | No | Force non-interactive mode. Auto-enabled when stdin is not a TTY. |
| --name | string | No | New project name. |
| --base-image | string | No | New base image. |
| --tier | seed \| fruit \| tree | No | New instance tier. |

<Callout>
  `guava deploy update` has been removed. Running it will display a message directing you to use `guava update` instead.
</Callout>

#### `guava self-upgrade`

Updates the `guava` binary itself to the latest released version. No parameters.

#### `guava deploy up`

| Name | Type | Required | Description |
|------|------|----------|-------------|
| dir | string | No | Optional path to the project directory. Defaults to the current directory. |
| --rebuild | flag | No | Force a fresh build even if code has not changed. |

#### `guava deploy down`

| Name | Type | Required | Description |
|------|------|----------|-------------|
| target | string | No | Optional project name or directory path. Defaults to the current directory. |
| --id | string | No | Optional task ID to stop directly. |

#### `guava deploy scale`

Scales an already-running deployment up or down by setting the desired pod-replica count. Use this to handle traffic spikes or scale back when idle without redeploying.

| Name | Type | Required | Description |
|------|------|----------|-------------|
| replicas | integer | Yes | Number of pod replicas. |
| dir | string | No | Project directory. Defaults to current directory. |

#### `guava deploy status`

| Name | Type | Required | Description |
|------|------|----------|-------------|
| target | string | No | Optional project name or directory path. If omitted, behaves like `deploy list`. |

#### `guava deploy logs`

| Name | Type | Required | Description |
|------|------|----------|-------------|
| target | string | No | Optional project name or directory path. Defaults to the current directory. |
| -n, --tail | integer | No | Number of log lines to retrieve (default 200, max 1000). |

#### `guava deploy build-logs`

| Name | Type | Required | Description |
|------|------|----------|-------------|
| target | string | No | Optional project name or directory path. Defaults to the current directory. |
| --id | string | No | Optional Cloud Build ID to look up directly. |

#### `guava deploy list`

Lists all deployments for the authenticated user as a table (NAME, NUMBER, ACTIVE, ID). `ls` is an alias for `list`.

#### `guava deploy changed`

| Name | Type | Required | Description |
|------|------|----------|-------------|
| target | string | No | Optional project name or directory path. Defaults to the current directory. |

Checks whether project files have changed since the last deploy.

#### `guava numbers list`

Lists every phone number your active org owns. `ls` is an alias for `list`. No parameters.

#### `guava numbers buy`

| Name | Type | Required | Description |
|------|------|----------|-------------|
| dir | string | No | Optional path to the project directory. Defaults to the current directory. |

Purchases a phone number and stores it in `guava.toml`. On the next deploy, the number is available to your code via the `GUAVA_AGENT_NUMBER` environment variable.

#### `guava conversations list`

Lists past conversations (calls) for your active org, newest first. All filters are optional and can be combined; results are paginated via `--after`.

| Name | Type | Required | Description |
|------|------|----------|-------------|
| --direction | inbound \| outbound | No | Filter by call direction. |
| --from | E.164 | No | Filter by from-number. |
| --to | E.164 | No | Filter by to-number. |
| --date-from | ISO 8601 date | No | Earliest start date (e.g. `2026-06-01`). |
| --date-to | ISO 8601 date | No | Latest start date. |
| -n, --limit | integer | No | Page size (1–100). Default 50. |
| --after | string | No | Pagination cursor (`next_cursor` from previous page). |

#### `guava conversations get` | `transcript` | `recording`

Inspect a single past call by ID — `get` returns the metadata and outcome, `transcript` returns the speaker-by-speaker text, and `recording` downloads the raw audio.

All take a positional `call_id`. `recording` also accepts `-o, --output <path>` (default `<call_id>.wav`).

#### `guava org`

Lists the orgs you belong to and lets you switch which one the CLI acts on. The active org determines which numbers, deployments, conversations, and campaigns the other commands see.

| Subcommand | Description |
|------------|-------------|
| `list` (alias `ls`) | List the orgs you belong to. Also the default when no subcommand is given (`guava org`). |
| `use [org_id]` | Switch the active org. Omit `org_id` to pick interactively from the list. |

#### `guava campaigns`

Manage outbound campaigns from the CLI: inspect their state, pause/resume/delete them, and upload contact lists. Each campaign is referenced by its `code`, a `gcmp-`-prefixed identifier (e.g. `gcmp-xxxxxxxxxxxxxxxxxxxx`) shown by `guava campaigns list` or on the dashboard.

| Subcommand | Description |
|------------|-------------|
| `list` (alias `ls`) | List outbound campaigns. |
| `show <code>` | Show campaign details and per-status contact counts. |
| `pause <code>` | Pause an in-flight campaign. |
| `resume <code>` | Resume a paused campaign. |
| `delete <code>` | Delete a campaign. |
| `add-contacts <code> <file> [flags…]` | Upload contacts to a campaign from a file (see below). |

**`add-contacts` arguments**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| code | string | Yes | Campaign code (e.g. `gcmp-...`). |
| file | path | Yes | Contacts file. Format dispatched by extension: `.csv` (header row with `phone_number`; other columns become string entries in each contact's `data`), `.json` (array of objects), `.jsonl` / `.ndjson` (one object per line). |
| --allow-duplicates | flag | No | Upload contacts that are already on the campaign. |
| --accept-tos | flag | No | Confirm the contacts comply with the [Terms of Service](https://goguava.ai/terms). Required in non-interactive mode; otherwise prompted. |

Every contact must include a `phone_number` field in E.164 format (e.g. `+15555550100`).

#### `guava widget`

Mints a short code that lets a browser-based WebRTC widget connect to your deployed agent. Use `--ttl` for a code that auto-expires; omit it for one that stays valid indefinitely.

| Name | Type | Required | Description |
|------|------|----------|-------------|
| --ttl | integer (seconds) | No | TTL for the WebRTC code. Omit for a permanent code. |

### The `guava.toml` file

Every Guava project has a `guava.toml` file in its root directory, created by `guava create` or `guava deploy up`. It stores the project's configuration and deployment state. The CLI reads it to identify the project and track deployments.

<Callout>
  Projects created with CLI versions 0.29.0 and lower will have a JSON `.guava` file. The CLI will automatically migrate to the new `guava.toml` on the next save.
</Callout>

<Callout>
  <span className="text-primary font-semibold">Do not edit this file manually.</span> Use `guava update` to change settings. Manually editing `guava.toml` can put your project into an inconsistent state and cause deploy failures.
</Callout>

<Callout>
  <span className="text-primary font-semibold">Commit this file to git.</span> If you delete it or leave it out of version control, the CLI won't be able to find your project or resume previous deployments.
</Callout>

<CodeBlock
  filename="guava.toml"
  language="toml"
  code={`project_id = "auto-generated unique ID"
name = "your project name"
base_image = "python-sandbox:3.14"
tier = "guava-seed"           # guava-seed | guava-fruit | guava-tree
replicas = 1
phone_number = "+15555555555"  # set by guava numbers buy or guava create --phone
call_direction = "inbound"     # inbound | outbound
org_id = "..."`}
/>

### Instance tiers

| Tier | CPU | Memory | Use case |
|------|-----|--------|----------|
| `guava-seed` | 1 | 1Gi | Development / testing |
| `guava-fruit` | 2 | 2Gi | Standard production |
| `guava-tree` | 4 | 4Gi | High-performance workloads |

### Base images & dependencies

When you create a project, you choose a base image. During deployment, your code and dependencies are built on top of this image to produce the final container that runs in the cloud. You can change the base image later with `guava update`.

Available base images:

- `python-sandbox:3.14`
- `python-sandbox:3.13`
- `python-sandbox:3.12`
- `python-sandbox:3.11`
- `python-sandbox:3.10`

Dependencies are installed from `pyproject.toml`, `requirements.txt`, `uv.lock`, or `poetry.lock`.

### Troubleshooting

- **"Please run `guava login`"** — Your session has expired or you haven't logged in yet. Run `guava login` to re-authenticate.
- **"No main.py found"** — `deploy up` requires a `main.py` in your project directory. Make sure you're running the command from the right folder.
- **Deploy didn't pick up my changes** — If nothing changed since the last deploy, the build step is skipped automatically. Use `guava deploy up --rebuild` to force a full rebuild.
- **Build failed** — Run `guava deploy build-logs` to see the full build output and diagnose the issue.
- **Missing files in deployment** — `deploy up` excludes `guava.toml`, hidden files/dirs, `__pycache__`, and `node_modules` from the upload. Make sure your files aren't in an excluded path.
- **Dependencies not installing** — The build system auto-detects dependencies from `pyproject.toml`, `requirements.txt`, `uv.lock`, or `poetry.lock`. Make sure one of these is present in your project root.

### Example

<CodeBlock
  filename="terminal"
  language="bash"
  code={`# Log in (opens browser)
guava login

# Create a new project
guava create my-agent --direction inbound

# Run locally
cd my-agent
guava run

# Deploy the project
guava deploy up

# Scale to 3 replicas
guava deploy scale 3

# View runtime logs
guava deploy logs -n 50

# Browse recent conversations
guava conversations list --direction inbound -n 20

# Download a call recording
guava conversations recording <call_id> -o call.wav

# Tear down
guava deploy down`}
/>

<NextLink section="webrtc-widgets" label="WebRTC Widgets" />

---

<!-- section: outbound-and-sms-permissions -->

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

## Outbound & SMS Permissions

These registrations get your organization approved to run outbound voice and SMS campaigns through Guava. They're required — you can't make outbound calls or send SMS without them — and several have prerequisite relationships, so the order matters. All registrations are managed from the [Guava Compliance page](https://app.goguava.ai/dashboard/compliance).

> Once you're approved, see [Phone Number Trust & Reputation](./phone-number-trust-reputation) for opt-in services that boost campaign deliverability.

Carriers and SMS aggregators require businesses to register before they can send outbound voice or SMS traffic — it's part of how the industry combats spam and fraud. Guava handles the submission and coordination on your behalf; you fill out a form on the Compliance page, and we route it to the right reviewer.

There are four registrations:

- [Outbound Dialing Registration](#outbound-dialing-registration) — *first step; one-time, prerequisite for everything else*
- [Use Cases](#use-cases) — *one per business purpose; required for SMS Campaign + Analytics Provider*
- [SMS Brand Registration](#sms-brand-registration) — *one-time, org-wide; required to send SMS*
- [SMS Campaign Registration](#sms-campaign-registration) — *one per use case; required to send SMS*

### Outbound Dialing Registration

Outbound Dialing Registration is the first step in the compliance process and a prerequisite for everything else. This is a Know Your Customer (KYC) form that collects your business identity, point of contact, compliance contact, call setup details, and telecom compliance information.

To get started, open the [Guava Compliance page](https://app.goguava.ai/dashboard/compliance) and fill out the Outbound Permissions form under General Forms. Once submitted, the form enters a review queue for the Guava team. Upon approval, your organization will be enabled for outbound dialing, and you'll gain access to the rest of the compliance registrations.

The form requires the submitter to have authority to control or manage the company and bind it to agreements. US-based businesses only; a valid EIN is required.

**What you'll need to provide:**

1. **Business Identity**: Legal business name, business type, industry, EIN, physical address, and website.
2. **Point of Contact**: The person authorized to enter into agreements on behalf of the company — name, email, phone, and job title.
3. **Compliance Contact**: A contact for compliance-related communications (may be the same as the point of contact).
4. **Call Setup**: The types of calls you plan to make (outbound, inbound, WebRTC), a description of your use cases, your phone number plan, and whether your use case involves PHI or cardholder data.
5. **Telecom Compliance**: FCC Robocall Mitigation Database (RMD) registration status, TCPA/TSR certification, consent method, Do Not Call Registry validation, and internal DNC list practices.

### Use Cases

A Use Case is a named grouping that ties together your campaigns, SMS campaign registration, and analytics provider attestation under a single business purpose. Each use case has a **name** (e.g., "Collections", "Appointments") and a **type** selected from a standard list of 45 categories (e.g., Abandoned Cart, Appointment Reminders, Lead Generation, Telehealth, Survey/Research).

Use cases exist because different outbound activities often serve different purposes, and compliance registrations — particularly SMS campaign registration and analytics provider attestation — are scoped per use case. For example, if your organization runs both appointment reminder campaigns and marketing campaigns, those are two separate use cases, each with their own SMS campaign registration and analytics attestation.

To create a use case, go to the [Guava Compliance page](https://app.goguava.ai/dashboard/compliance) and click "+ New Use Case" in the Use Cases section. You'll provide a name and select a type. You can then assign your existing campaigns to the use case and submit the SMS Campaign and Analytics Provider forms for it.

> You'll need to submit the Outbound Dialing Registration form before you can create use cases.

### SMS Brand Registration

SMS Brand Registration registers your business as an SMS brand with the carrier. This is a one-time, organization-wide registration — you only need to complete it once, regardless of how many campaigns or use cases you have. It's a prerequisite for sending SMS messages through Guava.

To register, go to the [Guava Compliance page](https://app.goguava.ai/dashboard/compliance) and fill out the SMS Brand Registration form under General Forms.

**What you'll need to provide:**

1. **Brand Details**: Legal company name, DBA/brand name, company type (Public, Private, US Nonprofit, US Government), EIN/Tax ID, and optional alternate identifiers (DUNS, GIIN, LEI).
2. **Address**: Street address, city, state, and zip code.
3. **Company Info**: Website, and stock symbol/exchange if publicly traded.
4. **Business Contact**: First and last name, email, and mobile phone.
5. **Support Contact**: Support email and phone number.

Once submitted, your brand registration will be forwarded to the carrier for processing. Upon approval, your organization will be assigned a business ID and your phone numbers will be associated with your brand.

> You'll need to submit the Outbound Dialing Registration form before the SMS Brand Registration form becomes available.

### SMS Campaign Registration

SMS Campaign Registration is an A2P (Application-to-Person) 10DLC campaign registration, completed once per use case. This registration tells the carrier what kind of SMS messages you'll be sending, how consumers opt in, and what your compliance keyword responses are (e.g., what happens when someone texts STOP or HELP).

To register, go to the [Guava Compliance page](https://app.goguava.ai/dashboard/compliance), find the relevant use case card, and click "SMS Campaign Form."

**What you'll need to provide:**

1. **Campaign Details**: Campaign type (from the use case type list), a description of the SMS campaign, how consumers opt in (call-to-action / message flow), CTA URL, terms of service URL, privacy policy URL, and sample messages.
2. **Compliance Keyword Responses**: Your opt-in keywords and response, opt-out (STOP) response, and help (HELP) response. Each response must include your brand name, campaign description, and support contact info.
3. **Usage Disclosures**: Whether you use number pooling (50+ numbers), direct lending, embedded links (note: public URL shorteners like bit.ly or tinyurl are not permitted), embedded phone numbers, or age-gated content. You'll also confirm the campaign won't be used for affiliate marketing.
4. **Traffic Profile**: SMS traffic type (transactional, customer care, marketing, or mixed), associated website, message frequency, and whether the website/domain is displayed in messages.

Once submitted, the registration will be forwarded to the carrier for processing. Upon approval, SMS will be enabled for your organization and your compliance keyword responses (opt-in, opt-out, help) will be configured.

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

### Summary

| Registration | Scope | Prerequisite | Info Needed |
|---|---|---|---|
| Outbound Dialing Registration | Once per org | — | Business identity, EIN, contacts, call setup, telecom compliance |
| Use Cases | One per business purpose | Outbound Dialing Registration | Name, type |
| SMS Brand Registration | Once per org | Outbound Dialing Registration | Brand details, address, EIN, business + support contacts |
| SMS Campaign Registration | One per use case | SMS Brand Registration + Use Case | Campaign details, opt-in flow, compliance responses, usage disclosures, traffic profile |

---

<!-- section: phone-number-trust-reputation -->

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

## Phone Number Trust & Reputation

> These are three ways to boost trust and reputation for the phone numbers in your outbound campaigns. None are required to run a campaign — they're all opt-in. Some we handle for you, some we set up on request, and one is a registration you complete yourself through a third-party service.
>
> If you're using your own number rather than a Guava-managed one, trust and reputation registration stays on your side as the number's owner — though we're happy to point you to helpful resources.

Call analytics providers and carriers analyze call metadata to score the phone numbers used in outbound campaigns. Numbers with low reputation scores often get blocked or flagged as spam before they ever reach the recipient. To help your campaigns connect successfully, we recommend three layers of trust and reputation work:

- [Analytics Provider Registration](#analytics-provider-registration) — *recommended; you register through a third-party service*
- [SHAKEN/STIR Protocol](#shakenstir-protocol) — *handled automatically by Guava*
- [Caller ID Name (CNAM) Registration](#caller-id-name-cnam-registration) — *set up by Guava on request*

### Analytics Provider Registration

Call analytics providers like [Transaction Network Services](https://tnsi.com/) (used by Verizon), [First Orion](https://firstorion.com) (used by T-Mobile), and [Hiya](https://www.hiya.com/) (used by AT&T) analyze call metadata to give phone numbers _reputation scores_. These reputation scores provide carriers with a probability of the call being spam or fraudulent, and carriers will often block phone numbers with low reputation scores from ever reaching the end user. Registering a business' phone numbers with the analytics providers increases their reputation scores and the likelihood of your calls connecting.

This isn't a service Guava provides directly — registration happens between your organization and the analytics providers. We recommend going through [Free Caller Registry](https://freecallerregistry.com), which submits your registration to all three major providers at once.

If you'd like, you can attest that you've completed this registration via the [Guava Compliance page](https://app.goguava.ai/dashboard/compliance) under the associated [Use Case](./outbound-and-sms-permissions#use-cases).

> For more general information about analytics providers and reputation scores, you can refer to the resources below:
>
> - Hiya Blog Article, ["How carriers label spam calls and how businesses can support a healthy call reputation"](https://blog.hiya.com/how-carriers-label-spam-calls-and-how-businesses-can-support-a-healthy-call-reputation)
> - First Orion Blog Article, ["Reasons Your Business Should Register Its Phone Number"](https://firstorion.com/reasons-your-business-should-register-its-phone-number/)
> - Blog Article, ["How Analytics Engines Determine Caller ID Scoring"](https://calleridreputation.com/blog/how-analytics-engines-determine-caller-id-scoring/)

### SHAKEN/STIR Protocol

SHAKEN (or, Signature-based Handling of Asserted Information Using toKENs) / STIR (or, Secure Telephone Identity Revisited) is a framework of interconnected standards, mandated by the FCC to combat the rise in unwanted robocalls and unlawful caller ID spoofing. When adopted, carriers can present a trust indicator, like "Caller Verified," to recipients' phones. You can think about it like a "handshake" that certifies the caller is known and trusted (similar to TLS handshakes and certificate authorities for websites).

We provide SHAKEN/STIR attestation automatically on all Guava-managed numbers — there's nothing to set up on your end.

> For more general information about the SHAKEN/STIR Protocol, you can refer to the resources below:
>
> - FCC Website, [Combating Spoofed Robocalls with Caller ID Authentication](https://www.fcc.gov/call-authentication)
> - Wikipedia, ["STIR/SHAKEN"](https://en.wikipedia.org/wiki/STIR/SHAKEN)
> - YouTube, ["What is SHAKEN/STIR, Call analytics and the role they play in the fight against unlawful robocalls"](https://www.youtube.com/watch?v=nMcJ4DUXAXo)

### Caller ID Name (CNAM) Registration

CNAM is a feature in the United States public telephone network that identifies an incoming caller by a personal or business name associated with the originating phone number. Your CNAM Display Name, typically a person's name or a company's name, will appear on landline phones by default and mobile phones when enabled by the subscriber.

If you'd like CNAM set up for your Guava-managed phone numbers, reach out to your account representative. We'll handle the registration for you — we just need a few details from your side:

- Submit the form for each phone number under Phone Numbers on the [Guava Compliance Page](https://app.goguava.ai/dashboard/compliance).
- If the Caller ID display name doesn't belong to your organization directly, but instead to another organization that has authorized your organization to make outbound calls on its behalf, send your account representative a supporting document (e.g., a signed authorization letter, service agreement, business registration document, or similar) demonstrating that authorization.

> Note: If no CNAM is set for a phone number, the default display on the recipient's device will be the City and State associated with the number, along with the number itself.

> For more general information about CNAM, you can refer to the resources below:
>
> - Wikipedia, ["Calling Name Presentation"](https://en.wikipedia.org/wiki/Calling_Name_Presentation)
> - Twilio Help Center, ["Getting Started with CNAM Caller ID"](https://help.twilio.com/articles/360051670533-Getting-Started-with-CNAM-Caller-ID)

### Summary

| Trust Service | Who handles it | Separate Registrations | Info Needed (if you opt in) |
|---|---|---|---|
| Analytics Provider Registration | You, via Free Caller Registry (we recommend) | For each use case | Use case, # of employees, avg. calls/day, phone numbers for the use case |
| SHAKEN/STIR Protocol | Guava (automatic) | — | None |
| CNAM | Guava (on request) | For each display name | Display name, associated phone numbers, possible supporting documentation |

---

<!-- section: deploy-heroku -->

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

export const AGENT_CODE = `import os
import guava

from typing_extensions import override
from guava import logging_utils

agent = guava.Agent(
    purpose="You are a helpful voice agent.",
    name="Hannah",
)

@agent.on_call_start()
def on_call_start(call: guava.Call):
    # Set your first task here using call.set_task(...)
    pass

if __name__ == "__main__":
    logging_utils.configure_logging()
    agent.listen_phone("+1...") # Replace with your agent's phone number.
`;

## Deploying to Heroku

Guava agents can be deployed to Heroku. They should be run as a background worker, rather than as part of a web server.

1. Add `guava-sdk` to your Heroku app.
2. Create an API Key on the Guava Dashboard.
3. Use the Heroku dashboard or CLI to add the key to your environment.
4. Add the following to your Procfile: `guava-worker: python agent.py` (or whichever file is your agent’s entrypoint).
5. Push your code and scale the new worker type.

What follows is a detailed guide.

### Add `guava-sdk` as a dependency

Add the Python package `guava-sdk` as a dependency using the package manager of your choice.

```shell
echo "guava-sdk==0.24.0" >> requirements.txt # If using pip + requirements.txt
uv add guava-sdk # If using uv
poetry add guava-sdk # If using poetry
```

### Add a Guava API Key to the Heroku Environment

Open the [API Keys page in the Guava dashboard](https://app.goguava.ai/dashboard/api-keys) and click **Create API Key**. The key should be of the form `gva-...`.

Next, add this key to your Heroku environment, either using the CLI or the Heroku dashboard.

```shell
heroku config:set GUAVA_API_KEY="gva-..."
```

### Define your agent

Define your agent. In this example we'll create it in a file `agent.py`, but you can use anything. Note the entrypoint `agent.listen_phone(...)` at the bottom - this method attaches your agent to the phone number and does not exit.

<CodeBlock filename="agent.py" language="python" code={AGENT_CODE} />

### Add the worker to your Procfile

Add the following section to your Procfile. This will create a new dyno type called "guava-worker".

```make
# Run the Guava agent in a background process.
guava-worker: python agent.py
```

### Push to Heroku

Commit and push your code to Heroku to trigger a deploy.

```shell
git add .
git commit -m "Added Guava agent."
git push heroku main # or master, depending on your setup
```

### Scale the Guava worker up

Scale the newly added worker dyno.

```shell
heroku ps:scale guava-worker=1
```

### Call your agent

Now, you should be able to call your agent. You can use `heroku logs` to check logs for your dynos.

<NextLink section="sip-integrations" label="SIP Integration" />

---

<!-- section: sip-integrations -->

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

## SIP Integrations

Guava agents can receive inbound calls over the SIP protocol. You can use this feature to directly dial Guava agents from an SBC, PBX, or softphone without going out to the PSTN.

<Callout>
<strong>Using Twilio?</strong> See the [Twilio Programmable Voice guide](/docs/twilio-programmable-voice) or the [Twilio Elastic SIP guide](/docs/twilio-elastic-sip) for step-by-step walkthroughs.
</Callout>

### Contact us for peer whitelisting

Currently, we are whitelisting SIP peers at our firewall level. Contact us at [hi@goguava.ai](mailto:hi@goguava.ai) to get your source IPs whitelisted.

### Check connectivity

Once whitelisted, try to make an `OPTIONS` ping to our SIP trunk at `sip.goguava.ai`. If the check fails, see our guide below on firewall configuration.

### Create a SIP code

Every SIP integration requires a `guavasip` code, which you can create in the Guava dashboard. `guavasip` codes work just like registered phone numbers — agents can listen to them and peers can dial them.

1. Open the [SIP page in the dashboard](https://app.goguava.ai/dashboard/sip).
2. Click **Create SIP Code**.
3. Take note of both the SIP code and the termination URI.

<CodeBlock
  language="bash"
  code={`# You will see a SIP code like this.
guavasip-xxx

# Your termination URI will look like this.
sip:guavasip-xxx@sip.goguava.ai`}
/>

### Attach your agent to the SIP code

Next, start an agent using `agent.listen_sip("guavasip-xxx")`. This is the SIP equivalent of `agent.listen_phone(...)` — Guava forwards every call addressed to that code to your agent.

<CodeBlock
  filename="main.py"
  language="python"
  code={`import os
import guava

agent = guava.Agent(
    name="Nova",
    organization="Acme Corp",
    purpose="Handle inbound calls from the corporate PBX.",
)

# Register handlers — on_call_received, on_call_start, etc.

# Replace with your SIP code from the dashboard.
agent.listen_sip("guavasip-xxx")`}
/>

<Callout>To connect an agent to both a phone number and a SIP code simultaneously, see the documentation for [guava.Runner](./runner).</Callout>

### Dial your agent

The last step is to dial your agent using the termination URI (e.g. `sip:guavasip-xxx@sip.goguava.ai`). If you're having trouble connecting to your agent, please contact us at [hi@goguava.ai](mailto:hi@goguava.ai).

### Firewall configuration

To dial our SIP trunk from your network, you may need to whitelist us in your firewall.

|     |  |
| -------- | ------- |
| **FQDN**  | The FQDN for the Guava SIP trunk is `sip.goguava.ai`.    |
| **Trunk IP** | The IP address for the Guava SIP trunk is `136.118.29.109`. This IP is used for both media and signaling.     |
| **TCP Ports**    | `5060`, `5061` (TLS)   |
| **UDP Ports** | `5060`, `10000-65535` (Media) |
| **ICMP** | Whitelist ICMP to allow connectivity checks (e.g. `ping`). |
| **Supported Codecs** | `PCMU` (G.711 μ-law), `PCMA` (G.711 a-law) |

---

<!-- section: twilio-programmable-voice -->

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

## Twilio Programmable Voice (TwiML)

<Callout><strong>Looking for Twilio Elastic SIP?</strong> Check out our [guide here](./twilio-elastic-sip).</Callout>

If you already use Twilio Programmable Voice / TwiML, you can transfer to a Guava agent at any time during your call.

1. Create a Guava SIP code "guavasip-xxx" [on the Guava dashboard](https://app.goguava.ai/dashboard/sip).
2. Attach an agent to the SIP code.
3. Use the following TwiML template to transfer to your agent.

<CodeBlock
  language="xml"
  code={`<Response>
    <Dial>
      <Sip>sip:guavasip-xxx@sip.goguava.ai</Sip>
    </Dial>
</Response>`}
/>

Below is a more detailed guide.

### Create a Guava SIP code

Every SIP integration in Guava requires a `guavasip` code. `guavasip` codes are used to route inbound calls to agents — agents can listen to codes and peers dial them.

Open the [SIP page in the Guava dashboard](https://app.goguava.ai/dashboard/sip) and click **Create SIP Code**.

<p style={{display: "flex", justifyContent: "center", margin: "1.5rem 0"}}>
    <img style={{maxWidth: "700px", width: "100%", borderRadius: "6px"}} src="/docs/sip-dashboard.png" alt="Guava SIP dashboard" />
</p>

Take note of the SIP code and the termination URI.

### Start an agent

Next, start an agent using `agent.listen_sip("guavasip-xxx")` — Guava forwards every call addressed to that code to your agent.

<CodeBlock
  filename="main.py"
  language="python"
  code={`import os
import guava

agent = guava.Agent(
    name="Nova",
    organization="Acme Corp",
)

# Register handlers — on_call_received, on_call_start, etc.

# Replace with your SIP code from the dashboard.
agent.listen_sip("guavasip-xxx")`}
/>

### Twilio Example: Outbound Call

Initiate an outbound call through Twilio, then use a `Dial` command to transfer the call to your Guava agent using the termination URI.

<CodeBlock
  filename="twilio_outbound.py"
  language="python"
  code={`import os
from twilio.rest import Client

client = Client(
    os.environ["TWILIO_ACCOUNT_SID"],
    os.environ["TWILIO_AUTH_TOKEN"]
)

client.calls.create(
    to="+1...", # The recipient of the call.
    from_="+1...", # Your owned Twilio number.
    twiml="<Response><Dial><Sip>sip:guavasip-xxx@sip.goguava.ai</Sip></Dial></Response>",
)`}
/>

---

<!-- section: twilio-elastic-sip -->

## Twilio Elastic SIP Integration

<Callout><strong>Looking for TwiML examples?</strong> Check out our [Twilio Programmable Voice / TwiML guide](./twilio-programmable-voice).</Callout>

If you already own a phone number in Twilio and want to connect it to a Guava agent, you can do so using Twilio Elastic SIP Trunking.

### Create a Guava SIP code

Every SIP integration in Guava requires a `guavasip` code. `guavasip` codes are used to route inbound calls to agents — agents can listen to them and peers can dial them.

Open the [SIP page in the Guava dashboard](https://app.goguava.ai/dashboard/sip) and click **Create SIP Code**.

Take note of the SIP code and the termination URI.

### Create a Twilio Elastic SIP trunk

Navigate to the "Elastic SIP Trunks" page on your Twilio Console. You can find it using the search bar in the top right.

<p style={{display: "flex", justifyContent: "center", margin: "1.5rem 0"}}>
    <img style={{maxWidth: "400px", width: "100%", borderRadius: "6px"}} src="/docs/twilio-elastic-sip-search.png" alt="Elastic SIP Trunks in the Twilio console" />
</p>

Click **Create new SIP Trunk** and provide the trunk with a name.

<p style={{display: "flex", justifyContent: "center", margin: "1.5rem 0"}}>
    <img style={{maxWidth: "700px", width: "100%", borderRadius: "6px"}} src="/docs/twilio-elastic-sip-trunks.png" alt="Twilio Elastic SIP Trunks page" />
</p>

### Set the Origination URI

On the trunk's settings page, click **Origination** on the left hand side. Then click **Add new Origination URI** and enter your Guava termination URI (e.g. `sip:guavasip-xxx@sip.goguava.ai`). Then click **Add**.

<p style={{display: "flex", justifyContent: "center", margin: "1.5rem 0"}}>
    <img style={{maxWidth: "700px", width: "100%", borderRadius: "6px"}} src="/docs/twilio-elastic-sip-origination.png" alt="Adding an origination URI in Twilio" />
</p>

### Assign a number to the Trunk

Assuming you already have a number in your Twilio account, under the **Develop** tab, go to **Phone Numbers** > **Manage** > **Active Numbers** and select that number. Select **Configure with** and change that setting to **SIP Trunk**. Then select the **SIP Trunk** dropdown and select the SIP trunk you created earlier.

<p style={{display: "flex", justifyContent: "center", margin: "1.5rem 0"}}>
    <img style={{maxWidth: "700px", width: "100%", borderRadius: "6px"}} src="/docs/twilio-elastic-sip-number-config.png" alt="Assigning a phone number to a SIP trunk in Twilio" />
</p>

Finally, scroll down and click **Save Configuration**.

### Start an agent

<CodeBlock
  filename="main.py"
  language="python"
  code={`import os
import guava

agent = guava.Agent(
    name="Nova",
    organization="Acme Corp",
    purpose="Handle inbound calls from the corporate PBX.",
)

# Register handlers — on_call_received, on_call_start, etc.

# Replace with your SIP code from the dashboard.
agent.listen_sip("guavasip-xxx")`}
/>

### Call your number

Call your number and you should be able to reach your agent.

---

<!-- section: cisco-cube-cucm -->

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

## Cisco CUBE / CUCM

Guava includes features purpose-built to support integration with Cisco enterprise contact center solutions, including Cisco Unified Border Element (CUBE) and Cisco Unified Communications Manager (CUCM).

Email [hi@goguava.ai](mailto:hi@goguava.ai) to request an integration guide.

---

<!-- section: amazon-connect-ai-customer-service -->

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

## AI Customer Service

This example shows how to route calls from an Amazon Connect contact flow to a Guava AI agent. Amazon Connect handles your inbound phone number and initial routing; the Guava agent (**Riley**) handles the customer conversation — answering questions from a knowledge base, collecting details, and escalating when needed.

Riley can:
- Answer product questions, return policies, shipping info, and warranty questions in real time using a knowledge base
- Collect customer details and create an Amazon Connect task for cases that need specialist follow-up
- Transfer the caller back to a live Connect agent queue for sensitive escalations (complaints, legal, "I want to speak to a human")

### How It Works

<MermaidDiagram chart={`flowchart TD
    A([Customer calls Amazon Connect]) --> B["Amazon Connect contact flow\n(greeting, optional IVR routing)"]
    B -->|Transfer to GUAVA_AGENT_NUMBER| C["Riley — Guava AI<br/>Greets caller · Answers questions · Collects details"]
    KB([knowledge base]) -.->|on_question| C
    C --> D{Outcome}
    D -->|Resolved on call| E[Wrap up call]
    D -->|Needs follow-up| F[Create Connect task]
    D -->|Needs live agent| G[Transfer back to Connect queue]
`} />

### Prerequisites

- Python 3.10 or later
- A **Guava account** with an API key and a phone number — sign up at [app.goguava.ai](https://app.goguava.ai)
- An **AWS account** with an Amazon Connect instance

### Step 1: Install Guava

Choose whichever package manager you prefer:

```bash
pip install guava-sdk    # Install using pip
uv add guava-sdk         # Install using uv
poetry add guava-sdk     # Install using poetry
```

Also install `boto3` for the Amazon Connect task API:

```bash
pip install boto3
```

### Step 2: Set Up Amazon Connect

#### 2a. Create or locate your Connect instance

1. Open the [Amazon Connect console](https://console.aws.amazon.com/connect/).
2. Create an instance if you don't have one, or select an existing one.
3. Note the **Instance ID** — it's the UUID at the end of the instance ARN:
   ```
   arn:aws:connect:us-east-1:123456789012:instance/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                                                   └─────────────── Instance ID ──────┘
   ```

#### 2b. Create a task contact flow

Riley creates Amazon Connect tasks for cases requiring follow-up. Tasks need a dedicated contact flow — the default queue flow doesn't support them.

1. In your Connect instance, go to **Routing → Contact flows → Create contact flow**.
2. Name it `Guava Task Flow` (or similar).
3. Add a **Set working queue** block and point it to your support queue.
4. Connect it to a **Transfer to queue** block, then a **Disconnect** block.
5. **Save and publish** the flow.
6. Open the flow, click **Show additional flow information**, and copy the **Contact flow ID** (UUID at the end of the ARN).

#### 2c. Note your support queue phone number

This is the number Riley will transfer escalations to — your live agent queue's direct dial number in Amazon Connect.

1. Go to **Routing → Phone numbers** in your Connect instance.
2. Use any number claimed to your instance that routes to your support queue, or claim a new one.
3. Copy the number in E.164 format (e.g., `+15551234567`).

#### 2d. Create the contact flow that routes to Riley

This is the contact flow your customers actually call into. It transfers them to Riley's Guava number.

1. Go to **Routing → Contact flows → Create contact flow** (type: Inbound contact flow).
2. Name it `AI Customer Service`.
3. Build the flow:

```
   Entry ──▶ Play prompt ──▶ Transfer to phone number ──▶ Disconnect
              "Thanks for              (GUAVA_AGENT_NUMBER)
              calling Pinnacle Gear.
              Please hold."
   ```

- Add a **Play prompt** block: set the text to a brief hold message (or skip it).
   - Add a **Transfer to phone number** block:
     - Set **Phone number** to your Guava agent number (the value of `GUAVA_AGENT_NUMBER`).
     - Set **Transfer type** to `Softphone transfer` or `PSTN`.
   - Add a **Disconnect** block on the error branch.

4. **Save and publish** the flow.
5. Assign this flow to an inbound phone number: go to **Channels → Phone numbers**, select a number, and set its contact flow to `AI Customer Service`.

<Callout>
**Adding routing logic**: If you want Connect to handle an IVR before routing to Riley, add a **Get customer input** block before the transfer. You can route product questions to Guava and billing escalations to a human queue based on the customer's menu selection.
</Callout>

### Step 3: Configure AWS Credentials

```bash
export AWS_ACCESS_KEY_ID="your-access-key-id"
export AWS_SECRET_ACCESS_KEY="your-secret-access-key"
export AWS_DEFAULT_REGION="us-east-1"   # Match your Connect instance region
```

The IAM user or role needs:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["connect:StartTaskContact"],
      "Resource": "arn:aws:connect:*:*:instance/YOUR_INSTANCE_ID/*"
    }
  ]
}
```

### Step 4: Set Environment Variables

```bash
# Guava
export GUAVA_API_KEY="your-guava-api-key"
export GUAVA_AGENT_NUMBER="+15551000000"      # Your Guava phone number (Riley's number)

# Amazon Connect
export CONNECT_INSTANCE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export CONNECT_CONTACT_FLOW_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"   # Task flow from Step 2b
export CONNECT_SUPPORT_QUEUE_NUMBER="+15551234567"   # Live agent queue from Step 2c
```

### Step 5: Run Riley

```bash
python -m examples.integrations.ccaas.amazon_connect.ai_customer_service
```

You should see:

```
INFO:ai_customer_service:Riley is ready — listening for inbound calls on +15551000000
```

Riley is now live. Call your Amazon Connect number (the one you assigned the `AI Customer Service` flow to in Step 2d) — Connect will transfer the call to Riley automatically.

---

### How the Code Works

#### Defining the agent

```python
agent = Agent(
    name="Riley",
    organization="Pinnacle Gear Co.",
    purpose="to help Pinnacle Gear customers with product questions, orders, returns, and warranty support",
)
```

The `Agent` is a top-level handle for your AI persona. You attach behavior to it with decorators (`@agent.on_call_start`, `@agent.on_question`, `@agent.on_task_complete(...)`) — each decorator registers a handler that runs when the corresponding event fires on a live call.

#### Accepting inbound calls

```python
agent.inbound_phone(os.environ["GUAVA_AGENT_NUMBER"]).run()
```

`inbound_phone` opens a persistent WebSocket to the Guava server and waits for calls. Each time a call arrives on Riley's number — whether from a customer dialing directly or transferred from Amazon Connect — the agent's registered handlers run on a fresh `Call` object that represents that conversation.

#### Kicking off the conversation

```python
@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
    call.set_task(
        "intake",
        objective="...",
        checklist=[
            guava.Say("Thanks for calling Pinnacle Gear. I'm Riley, and I'm here to help. ..."),
            guava.Field(key="customer_name", ...),
            guava.Field(key="inquiry", ...),
        ],
    )
```

`on_call_start` runs as soon as the call connects. We use it to set the first task — a named intake step the agent works through, greeting the caller and collecting the fields we need before deciding what to do next.

#### Answering questions with a knowledge base

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

Whenever a caller asks something Riley can't answer from context alone — "What's your return policy?", "Is this jacket machine washable?" — Guava invokes `on_question` with the question in natural language. `DocumentQA` retrieves the most relevant chunks from `SUPPORT_DOCS` and generates an accurate answer. Riley speaks the answer back and continues the conversation without any perceivable delay.

`DocumentQA()` with no `store` argument runs in **server mode** — your documents are uploaded to the Guava server and searched there. The `namespace` parameter scopes the uploaded documents to this agent so they don't collide with documents from other `DocumentQA` instances in the same account.

#### Routing after the conversation

```python
intent_recognizer = IntentRecognizer(
    {
        "live_agent": "Caller wants to speak with a human, agent, manager, or supervisor, or has a complaint, legal threat, or sensitive escalation",
        "follow_up_task": "Caller needs a return, refund, exchange, warranty claim, or has a damaged or wrong item that requires team follow-up",
        "resolved": "Caller's question was answered and no further action is needed",
    }
)

@agent.on_task_complete("intake")
def handle_inquiry(call: guava.Call) -> None:
    inquiry = call.get_field("inquiry", "")
    intent = intent_recognizer.classify(inquiry)

if intent == "live_agent":
        escalate_to_human(call)
    elif intent == "follow_up_task":
        collect_followup_details(call)
    else:
        call.hangup(...)
```

After the `intake` task finishes collecting details, the registered `@agent.on_task_complete("intake")` handler classifies the outcome using `IntentRecognizer` and routes accordingly. Using an LLM-based classifier handles natural phrasing correctly — "the zipper broke on the first use" maps to `follow_up_task` even without explicitly matching "broken". The recognizer is instantiated once at module level and reused across calls.

| Outcome | Example signals | Action |
|---|---|---|
| Resolved | Question answered, no action needed | Wrap up and end call |
| Follow-up | Return request, warranty claim | Collect email, create Connect task |
| Live agent | "I want to speak to someone", complaint | Transfer back to Connect |

#### Transferring back to Amazon Connect

```python
def escalate_to_human(call: guava.Call) -> None:
    call.transfer(
        os.environ["CONNECT_SUPPORT_QUEUE_NUMBER"],
        "Let the customer know you're connecting them with a specialist. "
        "Then transfer the call.",
    )
```

The second argument to `call.transfer` gives Riley natural language guidance on what to say before transferring — she'll let the customer know what's happening and transfer when it feels right in the conversation. The customer lands in the Connect support queue as a normal inbound call, which agents handle in the Contact Control Panel (CCP).

#### Creating a Connect task for follow-up

```python
connect_client.start_task_contact(
    InstanceId=os.environ["CONNECT_INSTANCE_ID"],
    ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
    Name=f"Follow-up Required — {call.get_field('customer_name')}"[:512],
    Description=f"Customer: ...\nEmail: ...\nOrder: ...\nIssue: ..."[:4096],
    Attributes={...},
)
```

When a caller needs follow-up but doesn't need a live agent right now, Riley collects their email and order number in a second task (`follow_up`). The `@agent.on_task_complete("follow_up")` handler then creates a task in Amazon Connect via `StartTaskContact`. Agents see it in the CCP like any other contact — with the customer's name, issue summary, and email for follow-up.

---

### Customization Ideas

**Use your own knowledge base**
Replace `SUPPORT_DOCS` with your actual product documentation, policy pages, or FAQs. `DocumentQA` handles chunking and retrieval automatically — paste in as much text as you need.

**Add IVR routing in Connect before transferring**
Instead of routing all calls to Riley, add a **Get customer input** block in your Connect flow. Route product questions to Riley's number and billing escalations to a human queue. Riley only handles the calls she's best suited for.

**Pass context from Connect to Riley via SIP**
For more advanced setups, Amazon Connect can transfer calls over a SIP trunk to a Guava SIP endpoint. This lets Connect pass the original Contact ID and other attributes as SIP headers, which the agent can read from the call info. You can then link Riley's Connect task back to the original call using `PreviousContactId` for full call chain reporting.

**Route different inquiry types to different agents**
Run multiple Guava agents on different agent numbers — one for product support, one for returns, one for technical help. Have Connect's IVR route to the right one based on the customer's menu selection.

**Use call info to customize behavior per call**
The `@agent.on_call_received` decorator lets you inspect the inbound `CallInfo` (caller number, SIP headers, etc.) before the call is accepted. Use this to accept or decline calls, or to set per-call variables in `on_call_start` that change Riley's behavior — for example, routing region-specific callers to region-specific transfer numbers.

### Complete Example

```python
import guava
import os
import logging
import json
import boto3
from datetime import datetime

from guava import Agent, logging_utils
from guava.helpers.openai import IntentRecognizer
from guava.helpers.rag import DocumentQA

logger = logging.getLogger("ai_customer_service")

# ---------------------------------------------------------------------------
# Knowledge base
# Guava answers caller questions from these documents in real time.
# Replace with your own product docs, policies, or FAQs.
# ---------------------------------------------------------------------------

SUPPORT_DOCS = """
Pinnacle Gear Co. — Customer Support Reference

RETURNS & REFUNDS
- We accept returns within 30 days of purchase for unused items in original packaging.
- Items showing signs of use may be exchanged for store credit at our discretion.
- Refunds are processed within 5–7 business days of receiving the returned item.
- Sale items are final sale and cannot be returned or exchanged.
- Start a return at pinnaclegear.com/returns or by calling our support line.

SHIPPING
- Standard shipping (5–7 business days): free on orders over $75, otherwise $7.99.
- Expedited shipping (2–3 business days): $14.99 flat rate.
- Overnight shipping: $29.99. Orders placed before 2 PM ET ship the same day.
- We ship to all 50 US states and Canada. International shipping is not available.

WARRANTY
- All products carry a 1-year limited warranty against manufacturing defects.
- Summit Series backpacks and Trail Pro footwear carry a lifetime warranty.
- Warranty claims require proof of purchase. Email support@pinnaclegear.com.
- Damage from misuse, normal wear, or accidents is not covered.

SIZING
- Apparel follows standard US sizing. See pinnaclegear.com/size-guide.
- Footwear runs true to size; for wide feet, size up by half a size.
- When between sizes, size up for layering or down for an athletic fit.

ORDERS & ACCOUNT
- Track orders at pinnaclegear.com/track using your order number and email.
- To modify or cancel an order, contact us within 1 hour of placing it.
- We accept Visa, Mastercard, Amex, Discover, PayPal, and Pinnacle gift cards.
- Pinnacle Rewards: 1 point per dollar spent; 100 points = $5 reward credit.

PRODUCT CARE
- Machine wash apparel on cold, gentle cycle. Tumble dry on low heat.
- Do not use fabric softener on moisture-wicking or DWR-coated gear.
- Re-apply DWR treatment after 10–15 wash cycles.
- Store sleeping bags loosely in a large cotton sack, never compressed long-term.
"""

document_qa = DocumentQA(documents=SUPPORT_DOCS, namespace="pinnacle-gear-customer-service")

connect_client = boto3.client("connect")

intent_recognizer = IntentRecognizer(
    {
        "live_agent": "Caller wants to speak with a human, agent, manager, or supervisor, or has a complaint, legal threat, or sensitive escalation",
        "follow_up_task": "Caller needs a return, refund, exchange, warranty claim, or has a damaged or wrong item that requires team follow-up",
        "resolved": "Caller's question was answered and no further action is needed",
    }
)

agent = Agent(
    name="Riley",
    organization="Pinnacle Gear Co.",
    purpose=(
        "to help Pinnacle Gear customers with product questions, orders, "
        "returns, and warranty support"
    ),
)

@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
    call.set_task(
        "intake",
        objective=(
            "Help the customer with their inquiry. Answer questions accurately using "
            "the knowledge base. If their issue needs a human specialist or involves "
            "a complaint or legal matter, let them know you'll connect them with someone."
        ),
        checklist=[
            guava.Say(
                "Thanks for calling Pinnacle Gear. I'm Riley, and I'm here to help."
            ),
            guava.Field(
                key="customer_name",
                description="Ask for the customer's name.",
                field_type="text",
                required=True,
            ),
            guava.Field(
                key="inquiry",
                description=(
                    "Understand what the customer needs. Answer their question if you can "
                    "using the knowledge base. If you cannot resolve it — they want a "
                    "return, refund, complaint resolution, or to speak with a person — "
                    "capture what they need in detail."
                ),
                field_type="text",
                required=True,
            ),
        ],
    )

@agent.on_question
def on_question(call: guava.Call, question: str) -> str:
    """Answer caller questions from the knowledge base in real time."""
    return document_qa.ask(question)

if intent == "live_agent":
        escalate_to_human(call)
    elif intent == "follow_up_task":
        collect_followup_details(call)
    else:
        call.hangup(
            "Thank the customer for calling Pinnacle Gear. Ask if there's "
            "anything else you can help with, then wish them a great day."
        )

def escalate_to_human(call: guava.Call) -> None:
    """Transfer the caller to the Amazon Connect support queue."""
    call.transfer(
        os.environ["CONNECT_SUPPORT_QUEUE_NUMBER"],
        "Let the customer know you're connecting them with a Pinnacle Gear "
        "specialist who can take care of them. Then transfer the call.",
    )

def collect_followup_details(call: guava.Call) -> None:
    """Collect contact info so a specialist can follow up via a Connect task."""
    call.set_task(
        "follow_up",
        objective=(
            "Collect the customer's contact details so our team can follow up "
            "and resolve their issue."
        ),
        checklist=[
            guava.Say(
                "I'll have our support team reach out to you directly to sort this out."
            ),
            guava.Field(
                key="email",
                description="Ask for their email address for our team to reach them.",
                field_type="text",
                required=True,
            ),
            guava.Field(
                key="order_number",
                description=(
                    "Ask for their order number if they have one. It's fine if they don't."
                ),
                field_type="text",
                required=False,
            ),
        ],
    )

@agent.on_task_complete("follow_up")
def create_followup_task(call: guava.Call) -> None:
    results = {
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "agent": "Riley",
        "organization": "Pinnacle Gear Co.",
        "use_case": "connect_inbound_ai_customer_service",
        "fields": {
            "customer_name": call.get_field("customer_name"),
            "inquiry": call.get_field("inquiry"),
            "email": call.get_field("email"),
            "order_number": call.get_field("order_number"),
        },
    }
    print(json.dumps(results, indent=2))
    logger.info("Follow-up details captured.")

try:
        connect_client.start_task_contact(
            InstanceId=os.environ["CONNECT_INSTANCE_ID"],
            ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
            Name=f"Follow-up Required — {call.get_field('customer_name', 'Customer')}"[:512],
            Description=(
                f"Customer: {call.get_field('customer_name')}\n"
                f"Email: {call.get_field('email')}\n"
                f"Order: {call.get_field('order_number', 'N/A')}\n"
                f"Issue: {call.get_field('inquiry')}"
            )[:4096],
            References={
                "source": {"Value": "guava_ai_customer_service", "Type": "STRING"},
            },
            Attributes={
                "customer_name": call.get_field("customer_name", ""),
                "email": call.get_field("email", ""),
                "order_number": call.get_field("order_number", ""),
            },
        )
        logger.info("Amazon Connect follow-up task created successfully.")
    except Exception as e:
        logger.error("Failed to create Amazon Connect task: %s", e)

call.hangup(
        "Let the customer know our support team will reach out by email within "
        "one business day. Thank them for their patience and wish them a great day."
    )

if __name__ == "__main__":
    logging_utils.configure_logging()
    logger.info(
        "Riley is ready — listening for inbound calls on %s",
        os.environ.get("GUAVA_AGENT_NUMBER", "(GUAVA_AGENT_NUMBER not set)"),
    )
    agent.inbound_phone(os.environ["GUAVA_AGENT_NUMBER"]).run()
```

---

<!-- section: amazon-connect-appointment-reminder -->

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

## Appointment Reminder

This example shows how to use Guava to call patients and remind them of upcoming appointments. If a patient needs to reschedule, the agent checks your Amazon Connect queue in real time — and either transfers them directly to a live scheduling agent (if one is free) or captures their callback preference and creates a Connect task (if the queue is busy).

### What Happens on the Call

<MermaidDiagram chart={`flowchart TD
    A([Guava calls patient]) --> B{Reach the right person?}
    B -->|No| C[Leave voicemail, end call]
    B -->|Yes| D{Can they make the appointment?}
    D -->|Yes| E[Send SMS confirmation]
    D -->|No| F{Check Connect queue}
    F -->|Agent free| G[Transfer live call]
    F -->|Queue busy| H[Collect callback preference]
    H --> I[Create Connect task for scheduler callback]
`} />

### Prerequisites

### Step 1: Install Guava

Choose whichever package manager you prefer:

```bash
pip install guava-sdk    # Install using pip
uv add guava-sdk         # Install using uv
poetry add guava-sdk     # Install using poetry
```

You also need `boto3` for the Amazon Connect API:

```bash
pip install boto3
```

### Step 2: Set Up Amazon Connect

If you already have a Connect instance configured, skip to the parts you haven't done yet.

#### 2a. Create a Connect Instance

1. Open the [Amazon Connect console](https://console.aws.amazon.com/connect/).
2. Click **Create instance** and follow the setup wizard.
3. Once the instance is created, open it and copy the **Instance ID** from the ARN shown in the overview — it's the UUID at the end:
   ```
   arn:aws:connect:us-east-1:123456789012:instance/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                                    This is your CONNECT_INSTANCE_ID
   ```

#### 2b. Create a Contact Flow for Task Routing

Amazon Connect tasks need a dedicated contact flow — the default queue flow does not support tasks.

1. In your Connect instance, go to **Routing → Contact flows → Create contact flow**.
2. Name it something like `Guava Task Routing Flow`.
3. Add a **Set working queue** block and connect it to your scheduling queue.
4. Add a **Transfer to queue** block after that.
5. Add a **Disconnect / hang up** block at the end of the error branch.
6. Save and publish the flow.
7. Open the flow, click **Show additional flow information**, and copy the **Contact flow ID** (the UUID at the end of the ARN).

<Callout>
**Tip:** For a minimal test flow, you can also use any existing inbound contact flow — tasks will be routed to whatever queue the flow sets.
</Callout>

#### 2c. Set Up a Scheduling Queue

1. Go to **Routing → Queues → Add new queue**.
2. Name it (e.g., `Scheduling`) and assign it to the hours of operation and outbound caller ID of your choice.
3. Copy the **Queue ID** from the queue's ARN (the UUID at the end).

#### 2d. Get a Transfer Number

This is the phone number Guava will transfer rescheduling patients to — typically your scheduling desk or the Amazon Connect direct dial number for your scheduling queue.

- In Connect, go to **Channels → Phone numbers** to see claimed numbers.
- Or use any external scheduling desk number in E.164 format (e.g., `+15551234567`).

### Step 3: Configure AWS Credentials

The example uses `boto3`, which reads credentials from the standard AWS credential chain. The easiest way for local development:

The IAM user or role you use needs the following permissions:
- `connect:GetCurrentMetricData` — to check queue availability
- `connect:StartTaskContact` — to create callback tasks

A minimal IAM policy:
```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "connect:GetCurrentMetricData",
        "connect:StartTaskContact"
      ],
      "Resource": "arn:aws:connect:*:*:instance/YOUR_INSTANCE_ID/*"
    }
  ]
}
```

### Step 4: Set Environment Variables

```bash
# Guava
export GUAVA_API_KEY="your-guava-api-key"
export GUAVA_AGENT_NUMBER="+15551000000"    # Your Guava phone number

# Amazon Connect
export CONNECT_INSTANCE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export CONNECT_CONTACT_FLOW_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export CONNECT_QUEUE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export CONNECT_TRANSFER_NUMBER="+15551234567"   # Scheduling desk number
```

### Step 5: Run the Example

```bash
python -m examples.integrations.ccaas.amazon_connect.appointment_reminder \
  +15559876543 \
  --name "Jane Doe" \
  --appointment "Thursday, April 17 at 10:30 AM"
```

Replace `+15559876543` with the phone number to call and adjust `--name` and `--appointment` as needed.

**What to expect:**
- Guava calls the patient and verifies they're the right person
- If they confirm: you'll see a confirmation logged and an SMS is sent
- If they need to reschedule: the example checks your Connect queue in real time
  - If agents are free: Guava transfers the patient to `CONNECT_TRANSFER_NUMBER`
  - If the queue is busy: Guava collects a callback preference and creates a task in your Connect instance — visible to agents in the Contact Control Panel (CCP)
- If no one picks up: Guava leaves a voicemail

---

### How It Works

#### 1. Defining the Agent and Kicking Off the Call

```python
agent = Agent(
    name="Alex",
    organization="Bright Valley Medical Center",
    purpose="to remind patients of their upcoming appointments and assist with rescheduling when needed",
)

agent.outbound_phone(
    from_number=os.environ["GUAVA_AGENT_NUMBER"],
    to_number=args.phone,
    variables={"patient_name": args.name, "appointment": args.appointment},
).run()
```

`Agent` is the top-level handle for the AI persona. `outbound_phone` places the call and binds the agent to that single conversation. The `variables` dict is passed into the call and made available throughout — handlers can read each value with `call.get_variable(key)`.

#### 2. Reaching the Right Person (`reach_person`)

```python
@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
    call.reach_person(contact_full_name=call.get_variable("patient_name"))
```

`reach_person` handles the gatekeeper problem — it has the agent confirm they're speaking with the intended patient before proceeding. If a family member answers, the agent politely asks for the patient by name. The reach result is delivered to the handler registered with `@agent.on_reach_person`.

```python
@agent.on_reach_person
def on_reach_person(call: guava.Call, outcome: str) -> None:
    if outcome == "unavailable":
        call.hangup("Leave a brief voicemail ...")
        return
    # otherwise, set up the reminder task
```

#### 3. Collecting the Appointment Response

```python
call.set_task(
    "remind",
    objective=...,
    checklist=[
        guava.Say(f"Hi {patient_name}, this is Alex calling from ..."),
        guava.Field(
            key="response",
            description="Ask if they can make the appointment or need to reschedule.",
            field_type="text",
            required=True,
        ),
    ],
)
```

`set_task` gives the agent a named objective and a checklist of items to work through. `guava.Field` tells the agent what information to collect — the patient might say "Yes, I'll be there" or "Actually, I have a conflict that day" — and the agent normalizes that into a structured value you can inspect via `call.get_field(...)`.

#### 4. Classifying the Patient's Response

```python
from guava.helpers.openai import IntentRecognizer

intent_recognizer = IntentRecognizer(["confirmed", "needs to reschedule"])

@agent.on_task_complete("remind")
def handle_response(call: guava.Call) -> None:
    response = call.get_field("response", "")
    if intent_recognizer.classify(response) == "needs to reschedule":
        handle_reschedule(call)
    else:
        ...
```

When the `remind` task completes, the registered `@agent.on_task_complete("remind")` handler runs. Rather than matching against a list of keywords, `IntentRecognizer` uses an LLM to classify what the patient actually meant — natural phrasing like "I actually have a conflict that day" or "Can we move it?" still maps to `needs to reschedule` without enumerating every variant. The recognizer is instantiated once at module level and reused across calls.

#### 5. Checking the Connect Queue in Real Time

```python
def check_queue_availability() -> dict:
    response = connect_client.get_current_metric_data(
        InstanceId=os.environ["CONNECT_INSTANCE_ID"],
        Filters={"Queues": [os.environ["CONNECT_QUEUE_ID"]], "Channels": ["VOICE"]},
        CurrentMetrics=[
            {"Name": "AGENTS_AVAILABLE", "Unit": "COUNT"},
            {"Name": "CONTACTS_IN_QUEUE", "Unit": "COUNT"},
        ],
    )
    ...
```

`GetCurrentMetricData` returns a real-time snapshot of the queue — how many agents are available and how many contacts are already waiting. This snapshot updates every ~15 seconds.

#### 6. Routing Decision: Transfer vs. Task

```python
queue = check_queue_availability()
if queue["agents_available"] > 0:
    call.transfer(os.environ["CONNECT_TRANSFER_NUMBER"], "...")
else:
    # collect callback preference, then create a task
    call.set_task("callback", ...)
```

If a scheduler is free, Guava transfers the call live. If not, the agent collects the patient's preferred callback time in a follow-up `callback` task; its completion handler creates a Connect task via `StartTaskContact` — complete with the patient's name, original appointment, and preferred callback window. The task appears in your agents' Contact Control Panel (CCP) just like any other contact.

#### 7. SMS Confirmation

```python
call_state = {"appointment_confirmed": False}

# ... inside the "remind" handler, when the patient confirms:
call_state["appointment_confirmed"] = True

# ... after agent.run() returns:
if call_state["appointment_confirmed"]:
    guava.Client().send_sms(...)
```

After `agent.run()` returns, if the patient confirmed, we send an SMS reminder via the Guava client. This runs after the call ends, so it never blocks the voice interaction.

---

### Customization Ideas

**Pull patient data from a database or CRM**
Replace the hardcoded `--name` and `--appointment` CLI args with a database query or CRM API call, and call patients in a loop:
```python
for patient in get_todays_appointments():
    agent.outbound_phone(
        from_number=os.environ["GUAVA_AGENT_NUMBER"],
        to_number=patient["phone"],
        variables={"patient_name": patient["name"], "appointment": patient["appointment"]},
    ).run()
```

**Set a queue depth threshold**
Instead of transferring whenever any agent is free, add a threshold — e.g., only transfer if fewer than 3 contacts are already waiting:
```python
if queue["agents_available"] > 0 and queue["contacts_in_queue"] < 3:
    call.transfer(...)
```

**Route by appointment type**
Different appointment types may need different queues or transfer numbers. Pass the appointment type into the call as a variable and route accordingly:
```python
transfer_number = SPECIALIST_LINE if call.get_variable("type") == "specialist" else GENERAL_LINE
call.transfer(transfer_number, "...")
```

**Add multi-language support**
Guava supports English, Spanish, French, German, and Italian. The agent's voice can be set per call using the persona configuration on the underlying `Call` object.

### Complete Example

```python
import guava
import os
import logging
import json
import argparse
import boto3
from datetime import datetime

from guava import Agent, logging_utils
from guava.helpers.openai import IntentRecognizer

logger = logging.getLogger("appointment_reminder")

connect_client = boto3.client("connect")

intent_recognizer = IntentRecognizer(["confirmed", "needs to reschedule"])

# State the SMS step needs after the call ends.
call_state: dict = {"appointment_confirmed": False}

agent = Agent(
    name="Alex",
    organization="Bright Valley Medical Center",
    purpose=(
        "to remind patients of their upcoming appointments and assist "
        "with rescheduling when needed"
    ),
)

def check_queue_availability() -> dict:
    """Query Amazon Connect for real-time agent availability in the scheduling queue."""
    try:
        response = connect_client.get_current_metric_data(
            InstanceId=os.environ["CONNECT_INSTANCE_ID"],
            Filters={
                "Queues": [os.environ["CONNECT_QUEUE_ID"]],
                "Channels": ["VOICE"],
            },
            CurrentMetrics=[
                {"Name": "AGENTS_AVAILABLE", "Unit": "COUNT"},
                {"Name": "CONTACTS_IN_QUEUE", "Unit": "COUNT"},
            ],
        )
        agents_available = 0
        contacts_in_queue = 0
        for result in response.get("MetricResults", []):
            for collection in result.get("Collections", []):
                name = collection["Metric"]["Name"]
                value = collection.get("Value") or 0
                if name == "AGENTS_AVAILABLE":
                    agents_available = int(value)
                elif name == "CONTACTS_IN_QUEUE":
                    contacts_in_queue = int(value)
        logger.info(
            "Queue check — agents available: %d, contacts in queue: %d",
            agents_available,
            contacts_in_queue,
        )
        return {"agents_available": agents_available, "contacts_in_queue": contacts_in_queue}
    except Exception as e:
        logger.error("Failed to fetch queue metrics: %s", e)
        return {"agents_available": 0, "contacts_in_queue": 0}

@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
    call.reach_person(contact_full_name=call.get_variable("patient_name"))

@agent.on_reach_person
def on_reach_person(call: guava.Call, outcome: str) -> None:
    if outcome == "unavailable":
        appointment = call.get_variable("appointment")
        call.hangup(
            "We were unable to reach the patient. Leave a brief, friendly voicemail from "
            "Bright Valley Medical Center reminding them of their appointment on "
            f"{appointment} and asking them to call back to confirm or reschedule."
        )
        return

patient_name = call.get_variable("patient_name")
    appointment = call.get_variable("appointment")
    call.set_task(
        "remind",
        objective=(
            f"Remind {patient_name} of their appointment at Bright Valley Medical "
            f"Center on {appointment}. Ask if they can attend or need to reschedule."
        ),
        checklist=[
            guava.Say(
                f"Hi {patient_name}, this is Alex calling from Bright Valley "
                f"Medical Center with a reminder about your appointment on {appointment}."
            ),
            guava.Field(
                key="response",
                description=(
                    "Ask if they can make the appointment or need to reschedule. "
                    "Capture their answer as 'confirmed' or 'reschedule'."
                ),
                field_type="text",
                required=True,
            ),
        ],
    )

@agent.on_task_complete("remind")
def handle_response(call: guava.Call) -> None:
    response = call.get_field("response", "")
    if intent_recognizer.classify(response) == "needs to reschedule":
        handle_reschedule(call)
    else:
        call_state["appointment_confirmed"] = True
        call.hangup(
            "Thank the patient for confirming. Remind them to arrive 15 minutes "
            "early and bring their insurance card and a photo ID. Wish them well."
        )

def handle_reschedule(call: guava.Call) -> None:
    queue = check_queue_availability()
    if queue["agents_available"] > 0:
        # Scheduling agents are free — transfer directly.
        call.transfer(
            os.environ["CONNECT_TRANSFER_NUMBER"],
            "Let the patient know you're connecting them with a scheduling specialist "
            "who will help find a new appointment time. Then transfer the call.",
        )
    else:
        # Queue is busy — collect callback preference and create a task.
        call.set_task(
            "callback",
            objective=(
                "Our scheduling team is currently busy. Collect the patient's preferred "
                "callback window so a scheduler can call them back."
            ),
            checklist=[
                guava.Say(
                    "Our scheduling team is with other patients right now. "
                    "I'll arrange for a scheduler to call you back."
                ),
                guava.Field(
                    key="preferred_callback",
                    description=(
                        "Ask when would be a good time for our scheduling team to call "
                        "them back. Capture their preferred day and time of day."
                    ),
                    field_type="text",
                    required=True,
                ),
            ],
        )

@agent.on_task_complete("callback")
def create_reschedule_task(call: guava.Call) -> None:
    patient_name = call.get_variable("patient_name")
    appointment = call.get_variable("appointment")
    preferred_callback = call.get_field("preferred_callback")

results = {
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "agent": "Alex",
        "organization": "Bright Valley Medical Center",
        "use_case": "appointment_reminder_reschedule",
        "fields": {
            "patient_name": patient_name,
            "original_appointment": appointment,
            "preferred_callback": preferred_callback,
        },
    }
    print(json.dumps(results, indent=2))
    logger.info("Reschedule details captured.")

try:
        connect_client.start_task_contact(
            InstanceId=os.environ["CONNECT_INSTANCE_ID"],
            ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
            Name=f"Reschedule Needed — {patient_name}"[:512],
            Description=(
                f"Patient: {patient_name}\n"
                f"Original appointment: {appointment}\n"
                f"Preferred callback: {preferred_callback}\n"
                "Action: Call patient back to reschedule their appointment."
            )[:4096],
            References={
                "source": {"Value": "guava_appointment_reminder", "Type": "STRING"},
            },
            Attributes={
                "patient_name": patient_name,
                "original_appointment": appointment,
                "preferred_callback": preferred_callback or "",
            },
        )
        logger.info("Amazon Connect reschedule task created successfully.")
    except Exception as e:
        logger.error("Failed to create Amazon Connect task: %s", e)

call.hangup(
        "Let the patient know a scheduling specialist will call them back at their "
        "preferred time. Thank them for their patience and wish them a great day."
    )

if __name__ == "__main__":
    logging_utils.configure_logging()

parser = argparse.ArgumentParser(
        description=(
            "Outbound appointment reminder with smart escalation for Bright Valley Medical Center. "
            "Confirms the appointment, transfers to a live scheduler if one is available, or "
            "creates an Amazon Connect callback task if the queue is busy."
        )
    )
    parser.add_argument("phone", help="Patient phone number in E.164 format (e.g. +15551234567)")
    parser.add_argument("--name", required=True, help="Full name of the patient")
    parser.add_argument(
        "--appointment",
        default="tomorrow at 9:00 AM",
        help="Appointment date and time string (default: 'tomorrow at 9:00 AM')",
    )
    args = parser.parse_args()

logger.info(
        "Calling %s (%s) — appointment: %s",
        args.name,
        args.phone,
        args.appointment,
    )

agent.outbound_phone(
        from_number=os.environ["GUAVA_AGENT_NUMBER"],
        to_number=args.phone,
        variables={"patient_name": args.name, "appointment": args.appointment},
    ).run()

# Send an SMS confirmation if the patient confirmed their appointment.
    if call_state["appointment_confirmed"]:
        try:
            guava.Client().send_sms(
                from_number=os.environ["GUAVA_AGENT_NUMBER"],
                to_number=args.phone,
                message=(
                    f"Hi {args.name}, this is Bright Valley Medical Center confirming "
                    f"your appointment on {args.appointment}. Please arrive 15 minutes early "
                    "and bring your insurance card and photo ID. Reply STOP to opt out."
                ),
            )
            logger.info("SMS confirmation sent to %s.", args.phone)
        except Exception as e:
            logger.error("Failed to send SMS confirmation: %s", e)
```

---

<!-- section: amazon-connect-csat-survey -->

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

## CSAT Survey

This example shows how to use Guava to call customers after a support interaction and collect a brief satisfaction survey — NPS score, resolution status, and open feedback. The results are written to your logs and posted back into Amazon Connect as a task linked to the original support contact, so the survey responses live alongside the conversation they're about.

The Guava agent (**Jamie**) can:
- Reach the right customer using `reach_person` (handles voicemail and the gatekeeper problem)
- Politely decline if the customer doesn't want to participate, without forcing them through the survey
- Collect an NPS score, resolution status, and open-ended feedback in a single conversational task
- Create an Amazon Connect task linked to the original `ContactId` via `RelatedContactId`, so the survey is reportable in Connect's contact search and historical metrics

### How It Works

<MermaidDiagram chart={`flowchart TD
    A([Guava calls customer]) --> B{Reach the right person?}
    B -->|No| C[End call politely]
    B -->|Yes| D{Willing to participate?}
    D -->|No| E[Thank them, hang up]
    D -->|Yes| F[Collect NPS, resolution, feedback]
    F --> G[Derive NPS category]
    G --> H[Create linked Connect task]
    H --> I[Thank customer, end call]
`} />

### Prerequisites

- Python 3.10 or later
- A **Guava account** with an API key and a phone number — sign up at [app.goguava.ai](https://app.goguava.ai)
- An **AWS account** with an Amazon Connect instance
- The **ContactId** of the original support call you want to survey about (Amazon Connect surfaces this in the Contact Control Panel and contact search)

### Step 1: Install Guava

Choose whichever package manager you prefer:

```bash
pip install guava-sdk    # Install using pip
uv add guava-sdk         # Install using uv
poetry add guava-sdk     # Install using poetry
```

You also need `boto3` for the Amazon Connect API:

```bash
pip install boto3
```

### Step 2: Set Up Amazon Connect

If you already have a Connect instance configured, skip to the parts you haven't done yet.

#### 2a. Create or locate your Connect instance

#### 2b. Create a contact flow for survey tasks

Amazon Connect tasks need a dedicated contact flow — the default queue flow does not support tasks.

1. In your Connect instance, go to **Routing → Contact flows → Create contact flow**.
2. Name it something like `Guava CSAT Task Flow`.
3. Add a **Set working queue** block and point it at the queue you'd like surveys to be reviewed in (a CX or QA queue is a natural fit).
4. Add a **Transfer to queue** block, then a **Disconnect** block.
5. **Save and publish** the flow.
6. Open the flow, click **Show additional flow information**, and copy the **Contact flow ID** (the UUID at the end of the ARN).

<Callout>
**Tip:** If you only want surveys to land somewhere queryable rather than being worked by a live agent, point the flow at a low-priority "review" queue — the task will still appear in Connect's historical reports and contact search.
</Callout>

### Step 3: Configure AWS Credentials

The example uses `boto3`, which reads credentials from the standard AWS credential chain. The easiest way for local development:

A minimal IAM policy:

### Step 4: Set Environment Variables

```bash
# Guava
export GUAVA_API_KEY="your-guava-api-key"
export GUAVA_AGENT_NUMBER="+15551000000"      # Your Guava phone number (Jamie's number)

# Amazon Connect
export CONNECT_INSTANCE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export CONNECT_CONTACT_FLOW_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"   # Survey task flow from Step 2b
```

### Step 5: Run the Example

```bash
python -m examples.integrations.ccaas.amazon_connect.csat_survey \
  +15559876543 \
  --name "Jane Doe" \
  --contact-id "11111111-2222-3333-4444-555555555555"
```

Replace `+15559876543` with the customer's phone number, set `--name` to their full name, and set `--contact-id` to the Amazon Connect `ContactId` of the original support call you're following up on.

**What to expect:**
- Guava calls the customer and verifies they're the right person
- Jamie asks if they have a minute for a quick survey
  - If they decline, Jamie thanks them and ends the call gracefully — no pressure
  - If they agree, Jamie collects an NPS score, asks whether the issue was resolved, and invites open-ended feedback
- The full survey result is printed as JSON to stdout and a Connect task is created **linked to the original support contact** via `RelatedContactId` — so the response shows up in contact search and historical reports tied to the conversation it's about
- If no one picks up, Guava ends the call politely

---

### How the Code Works

#### Defining the agent and starting the call

```python
agent = Agent(
    name="Jamie",
    organization="Pinnacle Gear Co.",
    purpose=(
        "to conduct a brief customer satisfaction survey about a recent "
        "support experience"
    ),
)

agent.outbound_phone(
    from_number=os.environ["GUAVA_AGENT_NUMBER"],
    to_number=args.phone,
    variables={
        "customer_name": args.name,
        "original_contact_id": args.contact_id,
    },
).run()
```

`Agent` is the top-level handle for the AI persona. `outbound_phone` places the call and binds the agent to that single conversation. The `variables` dict is passed into the call and made available to all handlers via `call.get_variable(key)` — here we pass the customer's name (so Jamie can address them) and the original Connect `ContactId` (so the resulting survey task can be linked back to the support call it's about).

#### Reaching the right person

```python
@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
    call.reach_person(contact_full_name=call.get_variable("customer_name"))

@agent.on_reach_person
def on_reach_person(call: guava.Call, outcome: str) -> None:
    if outcome == "unavailable":
        call.hangup("We were unable to reach the customer. End the call politely.")
        return
    # otherwise, set up the survey task
```

`reach_person` handles the gatekeeper problem — Jamie confirms she's speaking with the intended customer before starting the survey. If a family member or colleague answers, she politely asks for the right person. If the customer truly can't be reached, `outcome == "unavailable"` and we end the call without leaving a long voicemail (post-hoc surveys are easy to skip; we don't push them).

#### Asking permission, then collecting the survey

```python
call.set_task(
    "survey",
    objective=(
        "Conduct a short, friendly customer satisfaction survey about the customer's "
        "recent support call with Pinnacle Gear. Keep it conversational and brief."
    ),
    checklist=[
        guava.Say(f"Hi {customer_name}, this is Jamie from Pinnacle Gear. ..."),
        guava.Field(key="willing_to_participate", ..., required=True),
        guava.Field(key="nps_score", ..., field_type="integer", required=False),
        guava.Field(key="issue_resolved", ..., required=False),
        guava.Field(key="feedback", ..., required=False),
    ],
)
```

`set_task` gives Jamie a named objective and an ordered checklist. The first `Field` — `willing_to_participate` — is the gate. The remaining fields are marked `required=False` and the descriptions explicitly say *"Only ask this if they agreed to participate"* — so if the customer declines, Jamie doesn't push them through the rest of the survey. The agent reads the prompts as guidance and adapts its phrasing rather than reciting them verbatim, so the conversation stays natural even though the schema is structured.

#### Honoring the customer's choice

```python
participation_recognizer = IntentRecognizer(["willing to participate", "not willing to participate"])

@agent.on_task_complete("survey")
def save_survey(call: guava.Call) -> None:
    willing = call.get_field("willing_to_participate", "")
    if participation_recognizer.classify(willing) == "not willing to participate":
        call.hangup(
            "Respect their time and thank them for taking the call. "
            "Wish them a great day."
        )
        return
    # otherwise, save the survey
```

When the `survey` task completes, the `@agent.on_task_complete("survey")` handler runs. Customers respond with all kinds of phrasing — *"sure"*, *"now's not a great time"*, *"I'm driving"*, *"yeah I guess so"* — so we use `IntentRecognizer` to map the free-text answer to a clean `willing` / `not willing` decision. If they declined, we end the call with a thank-you. The recognizer is instantiated once at module level and reused across calls.

#### Deriving the NPS category

```python
try:
    nps_int = int(nps_raw) if nps_raw else 0
except (ValueError, TypeError):
    nps_int = 0
if nps_int >= 9:
    nps_category = "promoter"
elif nps_int >= 7:
    nps_category = "passive"
else:
    nps_category = "detractor"
```

NPS uses three buckets — promoters (9–10), passives (7–8), and detractors (0–6). We compute the category server-side rather than asking the agent to do it, so the categorization is consistent across every call and shows up in the Connect task title and attributes in a form you can filter on.

#### Linking the survey back to the original contact

```python
connect_client.start_task_contact(
    InstanceId=os.environ["CONNECT_INSTANCE_ID"],
    ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
    Name=f"CSAT Survey — {customer_name} ({nps_category})"[:512],
    Description=f"Customer: ...\nNPS Score: ...\nIssue Resolved: ...\nFeedback: ..."[:4096],
    RelatedContactId=original_contact_id,
    Attributes={
        "customer_name": customer_name,
        "nps_score": str(nps_raw or ""),
        "nps_category": nps_category,
        "issue_resolved": issue_resolved,
    },
)
```

The key field here is `RelatedContactId` — passing the original support call's `ContactId` tells Amazon Connect to link the new survey task to that conversation. In Connect's contact search, you can pivot from the support call to its survey task (and back) without joining tables yourself, and historical metrics that group by `RelatedContactId` will see them as a single customer interaction with a follow-up survey attached.

The `Attributes` dict is searchable in Connect's contact search — putting `nps_score`, `nps_category`, and `issue_resolved` here means CX leaders can filter and report on surveys directly inside the Connect console.

---

### Customization Ideas

**Trigger surveys automatically after every support call**
Instead of running this script manually, hook it into your support flow: after a Connect contact ends, fire a Lambda (via EventBridge on `Amazon Connect Contact Events`) that pulls the customer's name and phone from your CRM and invokes this example with the original `ContactId`. Surveys go out within minutes of the call ending, while the experience is fresh.

**Sample only a fraction of calls**
Calling every customer is overkill — most teams sample 10–20% of contacts. Add a random gate before invoking this script:
```python
import random
if random.random() < 0.15:
    run_csat_survey(...)
```

**Skip detractors of detractors**
If the customer was already escalated or marked dissatisfied during the original call (visible in Connect attributes on the original contact), skip the survey or route them to a manager call instead — surveying angry customers can make things worse.

**Push results to a data warehouse**
Replace the `print(json.dumps(results, indent=2))` line with a write to your warehouse of choice (BigQuery, Snowflake, Redshift) or push to S3 in JSON Lines for downstream analytics — the Connect task gives you the operational view, the warehouse gives you the analytical view.

**Customize the survey per product line**
Pass a `product_line` variable into the call and conditionally include extra fields in the checklist — e.g., apparel customers get asked about fit and sizing, footwear customers get asked about comfort and durability. The same agent persona can run very different surveys depending on what the customer just bought.

---

### Complete Example

```python
import guava
import os
import logging
import json
import argparse
import boto3
from datetime import datetime

from guava import Agent, logging_utils
from guava.helpers.openai import IntentRecognizer

logger = logging.getLogger("csat_survey")

connect_client = boto3.client("connect")

participation_recognizer = IntentRecognizer(["willing to participate", "not willing to participate"])

agent = Agent(
    name="Jamie",
    organization="Pinnacle Gear Co.",
    purpose=(
        "to conduct a brief customer satisfaction survey about a recent "
        "support experience"
    ),
)

@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
    call.reach_person(contact_full_name=call.get_variable("customer_name"))

@agent.on_reach_person
def on_reach_person(call: guava.Call, outcome: str) -> None:
    if outcome == "unavailable":
        call.hangup("We were unable to reach the customer. End the call politely.")
        return

customer_name = call.get_variable("customer_name")
    call.set_task(
        "survey",
        objective=(
            "Conduct a short, friendly customer satisfaction survey about the customer's "
            "recent support call with Pinnacle Gear. Keep it conversational and brief."
        ),
        checklist=[
            guava.Say(
                f"Hi {customer_name}, this is Jamie from Pinnacle Gear. "
                "We noticed you recently contacted our support team, and I was hoping "
                "to grab one minute of your time for a quick feedback survey. "
                "Your input really helps us improve."
            ),
            guava.Field(
                key="willing_to_participate",
                description=(
                    "Ask if they have about a minute to share some feedback. "
                    "Capture 'yes' or 'no'."
                ),
                field_type="text",
                required=True,
            ),
            guava.Field(
                key="nps_score",
                description=(
                    "On a scale of 1 to 10, how likely are they to recommend Pinnacle Gear "
                    "support to a friend or colleague? Only ask this if they agreed to participate."
                ),
                field_type="integer",
                required=False,
            ),
            guava.Field(
                key="issue_resolved",
                description=(
                    "Ask if their issue was fully resolved during the support call. "
                    "Capture 'yes', 'no', or 'partially'. Only ask if they agreed to participate."
                ),
                field_type="text",
                required=False,
            ),
            guava.Field(
                key="feedback",
                description=(
                    "Ask if there's anything we could have done better, or any other "
                    "comments they'd like to share. Only ask if they agreed to participate."
                ),
                field_type="text",
                required=False,
            ),
        ],
    )

@agent.on_task_complete("survey")
def save_survey(call: guava.Call) -> None:
    customer_name = call.get_variable("customer_name")
    original_contact_id = call.get_variable("original_contact_id")

willing = call.get_field("willing_to_participate", "")
    if participation_recognizer.classify(willing) == "not willing to participate":
        call.hangup(
            "Respect their time and thank them for taking the call. "
            "Wish them a great day."
        )
        return

nps_raw = call.get_field("nps_score")
    issue_resolved = call.get_field("issue_resolved", "unknown")
    feedback = call.get_field("feedback", "No additional feedback provided.")

# Derive NPS category from score.
    try:
        nps_int = int(nps_raw) if nps_raw else 0
    except (ValueError, TypeError):
        nps_int = 0
    if nps_int >= 9:
        nps_category = "promoter"
    elif nps_int >= 7:
        nps_category = "passive"
    else:
        nps_category = "detractor"

results = {
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "agent": "Jamie",
        "organization": "Pinnacle Gear Co.",
        "use_case": "post_call_csat_survey",
        "original_contact_id": original_contact_id,
        "fields": {
            "customer_name": customer_name,
            "nps_score": nps_raw,
            "nps_category": nps_category,
            "issue_resolved": issue_resolved,
            "feedback": feedback,
        },
    }
    print(json.dumps(results, indent=2))
    logger.info("CSAT survey results captured — NPS: %s (%s).", nps_raw, nps_category)

# Create a Connect task linked to the original support call via RelatedContactId.
    # This ties the survey results to the interaction for reporting without altering
    # the original contact's attributes.
    try:
        connect_client.start_task_contact(
            InstanceId=os.environ["CONNECT_INSTANCE_ID"],
            ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
            Name=f"CSAT Survey — {customer_name} ({nps_category})"[:512],
            Description=(
                f"Customer: {customer_name}\n"
                f"NPS Score: {nps_raw}/10 ({nps_category})\n"
                f"Issue Resolved: {issue_resolved}\n"
                f"Feedback: {feedback}"
            )[:4096],
            RelatedContactId=original_contact_id,
            References={
                "source": {"Value": "guava_csat_survey", "Type": "STRING"},
            },
            Attributes={
                "customer_name": customer_name,
                "nps_score": str(nps_raw or ""),
                "nps_category": nps_category,
                "issue_resolved": issue_resolved,
            },
        )
        logger.info("Amazon Connect CSAT task created and linked to contact %s.", original_contact_id)
    except Exception as e:
        logger.error("Failed to create Amazon Connect CSAT task: %s", e)

call.hangup(
        "Thank the customer sincerely for their feedback and time. "
        "Let them know their input helps the team improve. Wish them a great day."
    )

if __name__ == "__main__":
    logging_utils.configure_logging()

parser = argparse.ArgumentParser(
        description=(
            "Outbound post-call CSAT survey for Pinnacle Gear Co. "
            "Collects NPS score and feedback after a support interaction, then creates "
            "a linked Amazon Connect task tied to the original call for reporting."
        )
    )
    parser.add_argument("phone", help="Customer phone number in E.164 format (e.g. +15551234567)")
    parser.add_argument("--name", required=True, help="Full name of the customer")
    parser.add_argument(
        "--contact-id",
        required=True,
        help="Amazon Connect ContactId of the original support call to link the survey to",
    )
    args = parser.parse_args()

logger.info(
        "Calling %s (%s) for CSAT survey — original contact: %s",
        args.name,
        args.phone,
        args.contact_id,
    )

---

<!-- section: amazon-connect-product-support -->

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

## Product Support

This example shows how to route inbound product support calls from Amazon Connect to a Guava AI agent that can answer most questions on its own — pulling from a product FAQ — and create an Amazon Connect task when a real human needs to follow up. Unlike the AI Customer Service example, this one keeps the routing simple (no live transfer back to a queue) and focuses on two outcomes: **resolve the question on the call**, or **collect details and hand off to a specialist asynchronously**.

The Guava agent (**Jordan**) can:
- Greet the caller, take their name, and understand what they need
- Answer product, return, shipping, and warranty questions in real time using a product FAQ knowledge base
- Recognize when an issue requires human follow-up (returns, refunds, complaints, damaged items, manager requests) and collect the customer's email and order number for a specialist to reach out

### How It Works

<MermaidDiagram chart={`flowchart TD
    A([Customer calls Amazon Connect]) --> B["Amazon Connect contact flow\n(greeting, optional IVR)"]
    B -->|Transfer to GUAVA_AGENT_NUMBER| C["Jordan — Guava AI<br/>Greets caller · Takes name · Understands issue"]
    KB([Product FAQ knowledge base]) -.->|on_question| C
    C --> D{Outcome}
    D -->|Question answered| E[Wrap up call]
    D -->|Needs follow-up| F[Collect email + order number]
    F --> G[Create Connect task for specialist]
    G --> H[Promise email follow-up, end call]
`} />

### Prerequisites

### Step 1: Install Guava

Choose whichever package manager you prefer:

```bash
pip install guava-sdk    # Install using pip
uv add guava-sdk         # Install using uv
poetry add guava-sdk     # Install using poetry
```

You also need `boto3` for the Amazon Connect API:

```bash
pip install boto3
```

### Step 2: Set Up Amazon Connect

#### 2a. Create or locate your Connect instance

#### 2b. Create a contact flow for escalation tasks

Jordan creates Amazon Connect tasks for cases that need specialist follow-up. Tasks need a dedicated contact flow — the default queue flow doesn't support them.

1. In your Connect instance, go to **Routing → Contact flows → Create contact flow**.
2. Name it `Guava Support Task Flow` (or similar).
3. Add a **Set working queue** block and point it to your support queue.
4. Connect it to a **Transfer to queue** block, then a **Disconnect** block.
5. **Save and publish** the flow.
6. Open the flow, click **Show additional flow information**, and copy the **Contact flow ID** (the UUID at the end of the ARN).

#### 2c. Create the contact flow that routes to Jordan

This is the contact flow your customers actually call into. It transfers them to Jordan's Guava number.

1. Go to **Routing → Contact flows → Create contact flow** (type: Inbound contact flow).
2. Name it `Product Support`.
3. Build the flow:

```
   Entry ──▶ Play prompt ──▶ Transfer to phone number ──▶ Disconnect
              "Thanks for              (GUAVA_AGENT_NUMBER)
              calling Pinnacle Gear.
              Please hold."
   ```

4. **Save and publish** the flow.
5. Assign this flow to an inbound phone number: go to **Channels → Phone numbers**, select a number, and set its contact flow to `Product Support`.

<Callout>
**No live transfer queue here.** Unlike the AI Customer Service example, this example doesn't transfer escalations back to a live agent — it creates an asynchronous Connect task instead. If you'd rather offer a live transfer for escalations, see the AI Customer Service walkthrough for that pattern.
</Callout>

### Step 3: Configure AWS Credentials

The IAM user or role needs:

### Step 4: Set Environment Variables

```bash
# Guava
export GUAVA_API_KEY="your-guava-api-key"
export GUAVA_AGENT_NUMBER="+15551000000"      # Your Guava phone number (Jordan's number)

# Amazon Connect
export CONNECT_INSTANCE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export CONNECT_CONTACT_FLOW_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"   # Task flow from Step 2b
```

### Step 5: Run Jordan

```bash
python -m examples.integrations.ccaas.amazon_connect.product_support
```

You should see:

```
INFO:product_support:Jordan is ready — listening for inbound calls on +15551000000
```

Jordan is now live. Call your Amazon Connect number (the one you assigned the `Product Support` flow to in Step 2c) — Connect will transfer the call to Jordan automatically.

**What to expect:**
- Jordan greets the caller, takes their name, and asks what they need
- For straightforward questions ("How do I wash my jacket?", "What's your return window?", "Do you ship to Canada?"), Jordan answers from the product FAQ in real time and wraps up the call
- For requests that need a human (returns, refunds, complaints, damaged items, *"can I speak to a manager"*), Jordan collects the customer's email and order number, creates a Connect task with the issue summary, and tells the customer a specialist will email them within one business day

---

### How the Code Works

#### The product FAQ knowledge base

```python
# knowledge_base.py
PRODUCT_FAQ = """
Pinnacle Gear Co. — Product FAQ

RETURNS & REFUNDS
- We accept returns within 30 days of purchase ...

SHIPPING
- Standard shipping (5–7 business days): free on orders over $75 ...

WARRANTY
- All Pinnacle Gear products carry a 1-year limited warranty ...
...
"""
```

The knowledge base lives in a sibling `knowledge_base.py` file as a single multi-line string. Keeping it in its own module makes it easy to swap in a longer corpus, edit copy without touching the agent code, or load from a file at startup. There's no chunking or indexing required — `DocumentQA` handles all of that.

#### Wiring up the agent

```python
from knowledge_base import PRODUCT_FAQ

document_qa = DocumentQA(documents=PRODUCT_FAQ, namespace="pinnacle-gear-product-support")

agent = Agent(
    name="Jordan",
    organization="Pinnacle Gear Co.",
    purpose=(
        "to help customers with product questions, returns, shipping, "
        "and warranty inquiries"
    ),
)
```

`Agent` is the top-level handle for the AI persona. `DocumentQA` with no `store` argument runs in **server mode** — your documents are uploaded to the Guava server and searched there, so you don't need to set up GCP credentials or a vector store yourself. The `namespace` parameter scopes the upload to this agent so it doesn't collide with documents from other `DocumentQA` instances in the same account.

`DocumentQA` is content-addressed: if `PRODUCT_FAQ` hasn't changed since the last run, nothing is re-uploaded — startup is instant after the first launch.

#### Accepting inbound calls

```python
agent.inbound_phone(os.environ["GUAVA_AGENT_NUMBER"]).run()
```

`inbound_phone` opens a persistent WebSocket to the Guava server and waits for calls. Each time a call arrives on Jordan's number — whether dialed directly or transferred from the Amazon Connect contact flow — the agent's registered handlers run on a fresh `Call` object that represents that conversation.

#### Kicking off the conversation

```python
@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
    call.set_task(
        "intake",
        objective="Help the customer with their Pinnacle Gear question. ...",
        checklist=[
            guava.Say("Thank you for calling Pinnacle Gear. My name is Jordan. ..."),
            guava.Field(key="customer_name", ...),
            guava.Field(key="issue_summary", ...),
        ],
    )
```

`on_call_start` runs as soon as the call connects. We use it to set the first task — a named intake step Jordan works through, greeting the caller, taking their name, and capturing what they need in their own words. The `issue_summary` field's description tells Jordan to *try to resolve the question first* using the knowledge base, and only escalate if she can't — so simple FAQ questions get answered without ever entering an escalation flow.

#### Answering questions with a knowledge base

```python
@agent.on_question
def on_question(call: guava.Call, question: str) -> str:
    """Answer product questions from the knowledge base in real time."""
    return document_qa.ask(question)
```

Whenever a caller asks something Jordan can't answer from context alone — *"What's your return policy?"*, *"Is this jacket machine washable?"*, *"Do you ship overnight?"* — Guava invokes `on_question` with the question in natural language. `DocumentQA.ask` retrieves the most relevant chunks from the FAQ and generates an accurate answer. Jordan speaks the answer back and continues the conversation without any perceivable delay.

This decorator is what gives Jordan her FAQ superpowers: she'll never make up a return policy. If the FAQ doesn't cover the question, the answer will reflect that, and the customer naturally drops into the escalation path.

#### Routing after the conversation

```python
intent_recognizer = IntentRecognizer(
    {
        "escalation_needed": "Customer needs a return, refund, exchange, or complaint resolved, wants to speak with a manager or specialist, or has a damaged or broken item",
        "resolved": "Customer's question was answered and no further action is needed",
    }
)

@agent.on_task_complete("intake")
def handle_outcome(call: guava.Call) -> None:
    issue = call.get_field("issue_summary", "")
    if intent_recognizer.classify(issue) == "escalation_needed":
        collect_escalation_details(call)
    else:
        call.hangup("Thank the customer for calling Pinnacle Gear. ...")
```

After the `intake` task finishes, the registered `@agent.on_task_complete("intake")` handler classifies the outcome using `IntentRecognizer` and routes accordingly. Using an LLM-based classifier handles natural phrasing correctly — *"the seam ripped after one trip"* maps to `escalation_needed` even without explicitly matching "broken" or "return". The recognizer is instantiated once at module level and reused across calls.

| Outcome | Example signals | Action |
|---|---|---|
| Resolved | FAQ question answered, no action needed | Wrap up and end call |
| Escalation needed | Return, refund, complaint, damaged item, manager request | Collect email + order number, create Connect task |

#### Collecting follow-up details

```python
def collect_escalation_details(call: guava.Call) -> None:
    call.set_task(
        "escalation",
        objective="Collect the customer's contact details so a specialist can follow up.",
        checklist=[
            guava.Say("I'll have a specialist follow up with you directly to take care of that."),
            guava.Field(key="email", ..., required=True),
            guava.Field(key="order_number", ..., required=False),
        ],
    )
```

If the issue requires escalation, Jordan starts a second task to collect the email and order number. The order number is `required=False` because not every issue is order-bound (warranty inquiries on long-owned products, general complaints), and the field description explicitly tells Jordan that *"if they don't have one, that's fine"* — so she doesn't get stuck pestering the customer for something that doesn't exist.

#### Creating a Connect task for follow-up

```python
@agent.on_task_complete("escalation")
def create_support_task(call: guava.Call) -> None:
    ...
    connect_client.start_task_contact(
        InstanceId=os.environ["CONNECT_INSTANCE_ID"],
        ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
        Name=f"Support Escalation — {call.get_field('customer_name', 'Customer')}"[:512],
        Description=f"Customer: ...\nEmail: ...\nOrder: ...\nIssue: ..."[:4096],
        Attributes={
            "customer_name": call.get_field("customer_name", ""),
            "email": call.get_field("email", ""),
            "order_number": call.get_field("order_number", ""),
        },
    )
    call.hangup("Let the customer know a specialist will reach out by email ...")
```

Once the escalation task completes, the `@agent.on_task_complete("escalation")` handler creates a task in Amazon Connect via `StartTaskContact`. The task's `Description` carries the full issue summary, and its `Attributes` make the customer's name, email, and order number searchable in Connect's contact search — so support agents can pick up the work in the Contact Control Panel just like any other contact, with full context.

The handler also prints the captured fields as JSON for local debugging and ends the call with a promise of email follow-up within one business day.

---

### Customization Ideas

**Use your own product documentation**
Replace `PRODUCT_FAQ` with your real product manuals, policy pages, or help center articles. `DocumentQA` handles chunking and retrieval automatically — paste in as much text as you need, or load from files at startup:
```python
faq_text = pathlib.Path("docs/product_faq.md").read_text()
document_qa = DocumentQA(documents=faq_text, namespace="my-product-faq")
```

**Combine with a live-transfer escalation path**
This example only creates async tasks. If some escalations need a live agent immediately (high-value customers, urgent complaints), add a third intent class — e.g. `live_agent` — and use `call.transfer(...)` to send those calls back to a Connect queue, like the AI Customer Service example does.

**Look up customer info before answering**
If you can identify the caller from their phone number (CRM lookup), pass their order history into the call as variables and reference it from `on_call_start`. Jordan can then say *"I see your recent order shipped on Tuesday"* without the customer needing to provide an order number.

**Add real-time inventory or order lookups**
The `@agent.on_question` decorator can call any code, not just `DocumentQA`. Inspect the question, route product-availability questions to your inventory API, route order-status questions to your OMS, and fall back to the FAQ for everything else.

**Pass context from Connect to Jordan via SIP**
For more advanced setups, Amazon Connect can transfer calls over a SIP trunk to a Guava SIP endpoint. This lets Connect pass the original Contact ID and other attributes as SIP headers, which the agent can read from the call info — and you can link the resulting escalation task back to the original call using `RelatedContactId` for full call chain reporting (see the CSAT Survey example for the same `RelatedContactId` pattern).

---

### Complete Example

#### `__main__.py`

```python
import guava
import os
import sys
import logging
import json
import pathlib
import boto3
from datetime import datetime

from guava import Agent, logging_utils
from guava.helpers.openai import IntentRecognizer
from guava.helpers.rag import DocumentQA

logger = logging.getLogger("product_support")

# Load the product FAQ from the sibling knowledge_base module.
sys.path.insert(0, str(pathlib.Path(__file__).parent))
from knowledge_base import PRODUCT_FAQ

# Initialize DocumentQA in server mode — no GCP credentials needed.
# Documents are content-addressed: unchanged FAQ text is never re-uploaded.
document_qa = DocumentQA(documents=PRODUCT_FAQ, namespace="pinnacle-gear-product-support")

connect_client = boto3.client("connect")

intent_recognizer = IntentRecognizer(
    {
        "escalation_needed": "Customer needs a return, refund, exchange, or complaint resolved, wants to speak with a manager or specialist, or has a damaged or broken item",
        "resolved": "Customer's question was answered and no further action is needed",
    }
)

agent = Agent(
    name="Jordan",
    organization="Pinnacle Gear Co.",
    purpose=(
        "to help customers with product questions, returns, shipping, "
        "and warranty inquiries"
    ),
)

@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
    call.set_task(
        "intake",
        objective=(
            "Help the customer with their Pinnacle Gear question. Use the knowledge base "
            "to answer product questions accurately. If the customer needs a return, refund, "
            "or to speak with a specialist, collect their details for follow-up."
        ),
        checklist=[
            guava.Say(
                "Thank you for calling Pinnacle Gear. My name is Jordan. "
                "I can help with product questions, returns, shipping, and warranty. "
                "How can I help you today?"
            ),
            guava.Field(
                key="customer_name",
                description="Ask for the customer's name.",
                field_type="text",
                required=True,
            ),
            guava.Field(
                key="issue_summary",
                description=(
                    "Understand what the customer needs. Answer their question if you can "
                    "using the knowledge base. If you can't resolve it — return request, "
                    "refund, complaint, or request to speak with a specialist — note what "
                    "they need in detail."
                ),
                field_type="text",
                required=True,
            ),
        ],
    )

@agent.on_question
def on_question(call: guava.Call, question: str) -> str:
    """Answer product questions from the knowledge base in real time."""
    return document_qa.ask(question)

@agent.on_task_complete("intake")
def handle_outcome(call: guava.Call) -> None:
    issue = call.get_field("issue_summary", "")
    if intent_recognizer.classify(issue) == "escalation_needed":
        collect_escalation_details(call)
    else:
        call.hangup(
            "Thank the customer for calling Pinnacle Gear. Ask if there's anything "
            "else you can help with. If not, wish them a great day."
        )

def collect_escalation_details(call: guava.Call) -> None:
    call.set_task(
        "escalation",
        objective="Collect the customer's contact details so a specialist can follow up.",
        checklist=[
            guava.Say(
                "I'll have a specialist follow up with you directly to take care of that."
            ),
            guava.Field(
                key="email",
                description="Ask for the customer's email address.",
                field_type="text",
                required=True,
            ),
            guava.Field(
                key="order_number",
                description=(
                    "Ask for their order number if relevant to the issue. "
                    "If they don't have one, that's fine."
                ),
                field_type="text",
                required=False,
            ),
        ],
    )

@agent.on_task_complete("escalation")
def create_support_task(call: guava.Call) -> None:
    results = {
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "agent": "Jordan",
        "organization": "Pinnacle Gear Co.",
        "use_case": "inbound_product_support",
        "fields": {
            "customer_name": call.get_field("customer_name"),
            "issue_summary": call.get_field("issue_summary"),
            "email": call.get_field("email"),
            "order_number": call.get_field("order_number"),
        },
    }
    print(json.dumps(results, indent=2))
    logger.info("Escalation details captured.")

try:
        connect_client.start_task_contact(
            InstanceId=os.environ["CONNECT_INSTANCE_ID"],
            ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
            Name=f"Support Escalation — {call.get_field('customer_name', 'Customer')}"[:512],
            Description=(
                f"Customer: {call.get_field('customer_name')}\n"
                f"Email: {call.get_field('email')}\n"
                f"Order: {call.get_field('order_number', 'N/A')}\n"
                f"Issue: {call.get_field('issue_summary')}"
            )[:4096],
            References={
                "source": {"Value": "guava_product_support", "Type": "STRING"},
            },
            Attributes={
                "customer_name": call.get_field("customer_name", ""),
                "email": call.get_field("email", ""),
                "order_number": call.get_field("order_number", ""),
            },
        )
        logger.info("Amazon Connect support task created successfully.")
    except Exception as e:
        logger.error("Failed to create Amazon Connect task: %s", e)

call.hangup(
        "Let the customer know a specialist will reach out by email within one business "
        "day. Thank them for their patience and wish them a great day."
    )

if __name__ == "__main__":
    logging_utils.configure_logging()
    agent.inbound_phone(os.environ["GUAVA_AGENT_NUMBER"]).run()
```

#### `knowledge_base.py`

This file contains the product FAQ document imported by the main script.

```python
PRODUCT_FAQ = """
Pinnacle Gear Co. — Product FAQ

RETURNS & REFUNDS
- We accept returns within 30 days of purchase for unused items in original packaging.
- Items showing signs of use may be returned for store credit only, at our discretion.
- To start a return, visit pinnaclegear.com/returns or call our support line.
- Refunds are processed within 5–7 business days after we receive the returned item.
- Sale items are final sale and cannot be returned or exchanged.
- Gift recipients can exchange items for equal or lesser value without a receipt.

SHIPPING
- Standard shipping (5–7 business days): free on orders over $75, otherwise $7.99.
- Expedited shipping (2–3 business days): $14.99 flat rate.
- Overnight shipping (next business day): $29.99. Orders placed before 2 PM ET ship same day.
- We ship to all 50 US states and Canada. International shipping is not yet available.
- Orders placed before 2 PM ET on business days ship the same day.

WARRANTY
- All Pinnacle Gear products carry a 1-year limited warranty against manufacturing defects.
- Our Summit Series backpacks and Trail Pro footwear carry a lifetime warranty.
- Warranty claims require proof of purchase. Contact support@pinnaclegear.com to file a claim.
- Normal wear and tear, damage from misuse, or accidental damage are not covered under warranty.

SIZING
- Apparel follows standard US sizing. See our full size guide at pinnaclegear.com/size-guide.
- Footwear runs true to size. For wide feet, we recommend sizing up by half a size.
- Backpack sizing is based on torso length, not height. Measure from your C7 vertebra to your iliac crest.
- When between sizes in apparel, size up for layering and size down for a fitted athletic look.

PRODUCT CARE
- Most Pinnacle apparel is machine washable on cold, gentle cycle. Tumble dry on low heat.
- Do not use fabric softener on moisture-wicking or DWR-coated items — it reduces performance.
- Down insulation should be washed on a delicate cycle and dried on low heat with clean tennis balls.
- Re-apply DWR water repellent treatment (such as Nikwax TX.Direct) after 10–15 wash cycles.
- Store sleeping bags loosely in a large cotton storage sack — never compressed for long periods.
- Clean tents with mild soap and cool water. Never machine wash or put a tent in the dryer.

ORDERS & ACCOUNT
- Track your order at pinnaclegear.com/track using your order number and the email on the order.
- To modify or cancel an order, contact us within 1 hour of placing it. After that, it may have shipped.
- We accept Visa, Mastercard, American Express, Discover, PayPal, and Pinnacle gift cards.
- Pinnacle Rewards members earn 1 point per dollar spent. 100 points = $5 reward credit.
- To create an account or reset your password, visit pinnaclegear.com/account.
"""
```

---

<!-- section: release-notes -->

## Release Notes

What's new in Guava.

---

### July 14, 2026

Guava 0.34.0 brings phone number filtering and navigation enhancements to the dashboard, adds SIP header forwarding to SDK event handlers, and improves CLI authentication feedback.

#### New Features

- SIP headers are now included in SDK call event payloads, making them accessible from event handlers.
- The CLI authentication flow now shows clearer success and error feedback after the OAuth callback.

#### Improvements

- The Conversations view in the dashboard can now be filtered by phone number, with the filter state preserved in the URL for shareable, bookmarkable links.
- Navigating back from a conversation detail page now highlights and scrolls to the previously viewed entry in the list.
- Call detail pages now display the reason a conversation ended.

#### Bug Fixes

- Transcripts now correctly truncate at the point of an interruption, removing speech that was cut off before the caller finished speaking.

---

### July 7, 2026

Guava 0.33.0 streamlines CLI workflows with direct project targeting, establishes `agent.roleplay()` as the canonical testing method name, and adds automated, voice-based "Do Not Call" detection and flagging for outbound campaigns.

#### New Features

- `guava deploy` and `guava update` now accept a `project_id` argument directly. The previous `task_id` parameter has been removed. See the [CLI Reference](/docs/cli-reference).
- `agent.roleplay()` is now the canonical method name for running simulated test calls, replacing `agent.test_roleplay()`. The previous name continues to work as an alias. See [Agent Testing](/docs/agent-testing).
- Voice-based Do Not Call (DNC) detection is now available for outbound campaigns. Callers who verbally opt out are automatically flagged, and a corresponding SDK event is emitted.

---

### June 30, 2026

Guava 0.32.0 adds outbound campaign support to the TypeScript SDK, refines outbound voicemail detection, and introduces filtering by status for campaign conversations.

#### New Features

- The [`attach_campaign`](/docs/campaign) method is now available in the TypeScript SDK, bringing parity with the Python SDK for campaign-based agent workflows.
- Campaign conversations can now be filtered by status in the dashboard.

#### Improvements

- Voicemail detection on outbound calls has been improved, driving greater accuracy of campaign statistics.

#### Bug Fixes

- Fixed `attach_campaign` in the Python SDK not exiting cleanly on Ctrl-C.
- Fixed datetime fields in the `CallAttempt` API response not being serialized correctly.

---

### June 24, 2026

Guava 0.31.0 adds DTMF (keypress tones) support to the TypeScript SDK, introduces datetime filtering for campaign calling attempts, and enables bundling of documentation directly into new projects.

#### New Features

- [`on_dtmf`](/docs/on-dtmf) is now supported in the TypeScript SDK, enabling agents to handle caller keypresses in TypeScript projects.
- `guava create` now downloads the Guava documentation bundle into new projects, giving immediate access to SDK reference material.
- Campaign attempts can now be filtered by a datetime range, making it easier to query attempt history for specific time windows.

#### Bug Fixes

- Fixed the `guava login` command holding TCP ports open if the OAuth callback fails.

---

### June 16, 2026

Guava 0.30.0 adds a new chat widget supporting audio and text, inbound SMS support, in-CLI management of outbound campaigns, Do Not Call list checks for outbound campaigns, and an updated intent recognizer.

#### New Features

- A new multimodal widget (`guava-widget-audio-chat`) supports audio and text chat within a single session.
- Agents can now send DTMF digits during a call using `call.set_agent_dtmf(enabled=True)`.
- [`on_session_end`](/docs/on-session-end) handlers now receive a `termination_reason` field describing why the session ended, available in both the Python and TypeScript SDKs.
- [`GET /v1/conversations`](/docs/conversations-api) now accepts phone-number query parameters to filter results by caller or callee.
- Reach-person detection has been updated to v2 with improved accuracy.
- Agents can read incoming messages via a new inbox API endpoint and the `next_sms` (Python) / `nextSms` (TypeScript) [SDK methods](/docs/messaging).
- `Agent.chat` mode in the TypeScript SDK enables text-based (non-voice) conversation sessions.
- [`MockCall`](/docs/agent-testing) and per-handler unit test support have been added to the TypeScript SDK, allowing agent handlers to be tested in isolation.
- SDK agents now attach to an existing [campaign](/docs/campaign) via `campaign_code`. Create campaigns through the API or dashboard before starting an agent.
- Do Not Call (DNC) list support has been added to campaigns; numbers on the list are automatically skipped when dialing.
- Contacts API v2 adds expanded filtering and batch management capabilities.
- A new [`IntentRecognizer`](/docs/intent-helpers) class in `guava.helpers.llm` uses Guava's LLM endpoint for intent recognition.
- OpenAI-compatible wrappers have been added for [document QA](/docs/document-qa) and [vector store](/docs/vector-stores) workflows. VertexAI helpers have been renamed to GenAI helpers (`guava.helpers.genai`).
- New CLI subcommands: `guava contact upload`, `guava conversations`, and `guava campaigns`.
- `guava login` now accepts a `--no-launch-browser` flag for headless and CI environments.
- CLI configuration is now stored as a plain TOML file, making it easy to inspect and edit by hand.
- `guava deploy` now includes a dashboard view showing the status of active deployments.
- Campaigns can now be created and edited directly from the dashboard.

#### Improvements

- Updated `guava create` project templates. See the [CLI Reference](/docs/cli-reference).
- Outbound dialing and SMS compliance forms have been updated with bug fixes and improved field syncing.
- A `skipDeprecationCheck` option has been added to the TypeScript SDK client constructor.

---

### June 9, 2026

Guava 0.29.0 introduces text-based chat sessions, an agent testing framework, CLI self-upgrade, and TypeScript SDK Transport v2.

#### New Features

- Agents can now conduct text-based chat sessions in addition to voice calls.
- An initial [agent testing framework](/docs/agent-testing) has been added to the Python SDK, including `guava.testing.MockCall` for mocking call interactions.
- The `guava` CLI now supports self-upgrade via `guava self-upgrade`, updating the binary in place. See the [CLI Reference](/docs/cli-reference).
- The TypeScript SDK has been updated to Transport v2, with a rewritten socket layer, updated event types, and improved inbound call handling.
- The TypeScript SDK now supports CLI-based authentication, removing the need to pass API keys explicitly when the `guava` CLI is in use.
- The LLM helper (`guava.helpers.llm`) in the Python SDK now authenticates via CLI credentials.
- Added [outbound scheduling](/docs/outbound-scheduling) examples to the Python SDK with `--local` flag support.

#### Improvements

- Phone numbers are now displayed and formatted correctly during `guava create` flows.
- Improved error handling when agent code throws exceptions during execution.
- Improved call screener resilience against transient errors in both the Python and TypeScript SDKs.

#### Bug Fixes

- Fixed `guava deploy up` incorrectly defaulting to an inbound deployment when run on an existing project without a `.guava` configuration file.

---

### June 2, 2026

Guava 0.28.0 brings smarter intent handling, automated SMS follow-up for unanswered campaign calls, and CLI improvements.

#### New Features

- Agents now handle ambiguous user intents more robustly. A new [intent helper](/docs/intent-helpers) method returns a list of plausible matches, and the [`on_action_request`](/docs/on-action-request-execute) callback accepts this list to naturally disambiguate with the caller.
- [Agentic campaigns](/docs/agentic-tenacity) now automatically send an SMS follow-up when a call goes unanswered, informing the recipient of the attempt to reach out.
- `guava run` now accepts additional arguments to pass through to the running agent. See the [CLI Reference](/docs/cli-reference).
- The TypeScript SDK now supports local calling.

#### Improvements

- The [WebRTC Helper](/docs/webrtc-widgets) has been updated to v0.2.0, fixing slow bot audio and Bluetooth headset connection issues.
- The API and Python SDK now warn when attempting to stop a [campaign](/docs/campaign) that has already dispatched calls.

#### Bug Fixes

- Fixed a crash triggered by multiple action requests or questions said at once.
- Fixed a bug where admin users were unable to delete deployments from the dashboard.
- Fixed an issue where inbound calls could be routed incorrectly.

---

### May 27, 2026

WebRTC support arrives in the TypeScript SDK, agentic tenacity comes to campaigns with SMS, and CLI messaging is refined.

#### New Features

- The TypeScript SDK now includes [WebRTC](/docs/webrtc-widgets) support, with an updated `property-insurance` example to get started.
- Python SDK examples now open an interactive phone number picker when run in an interactive terminal session.
- The `guava deploy` command now provides clearer, more informative output messages.
- [Agentic tenacity](/docs/agentic-tenacity) is now available for campaigns with SMS enabled.

#### Bug Fixes & Improvements

- Updated the [quickstart guide](/docs/quickstart) with separate install method sections and added agent test and stop steps.
- The docs section now has its own dedicated navigation bar.
- Various website fixes and content updates.

---

### May 19, 2026

Guava introduces new and improved tools for measuring outbound calling campaign performance.

#### New Features

- A new **Campaign Stats Endpoint** enables querying of call outcome statistics, with support for filtering by multiple statuses. See the [Campaign](/docs/campaign) reference for details.
- A **PowerShell install script** has been added for Windows [CLI](/docs/cli-reference) installation.

#### Bug Fixes & Improvements

- Voicemail detection accuracy is improved. "Voicemail" as a call termination reason is now reported by agents and tracked in autodialer campaigns, yielding better campaign outcomes data. See [`set_voicemail_action()`](/docs/set-voicemail-action) for related configuration.
- The process for approving outbound calling and SMS agent workflows has been streamlined. Read more about requesting outbound permissions [here](/docs/outbound-and-sms-permissions).
- Various refinements to ASR (speech recognition), TTS (text-to-speech), and background noise handling.

---

### May 12, 2026

The Guava CLI makes gains in utility and flexibility. TTS pronunciation is improved.

#### New Features

- `guava create` and `guava update` now support non-interactive mode, making it easier for scripts, CI pipelines, and agents to run commands. See the [CLI Reference](/docs/cli-reference).
- Added `guava numbers list` to list phone numbers from the [CLI](/docs/cli-reference).
- Added `guava run` to run your agent project locally. See the [CLI Reference](/docs/cli-reference).
- The Guava CLI is now available as a Windows binary. See the [CLI Reference](/docs/cli-reference).
- Added [`get_var` / `set_var`](/docs/get-set-variable) as shorter aliases for `get_variable` / `set_variable` in the Guava Agent API.
- `call_info` is now accessible directly on the `call` object.
- The Python SDK can now authenticate using CLI credentials.

#### Bug Fixes & Improvements

- For spelling back words (e.g. "I have that as 'Guava, spelled G-U-A-V-A'"), TTS pronunciation and rate of speech are improved.

---

### May 5, 2026

The Guava Agent API gains voice configuration, DTMF event support, and call ID access. Campaigns now support custom caller IDs for pre-approved accounts.

#### New Features

- Added a `voice` parameter to the [Agent](/docs/agent) class.
- Exposed `call.id` for use in [agent](/docs/agent) code.
- Added support for DTMF events in the Guava Agent API.
- Pre-approved accounts can set their own number as the caller ID for outbound calls. See the [Campaign](/docs/campaign) reference.
- `guava deploy` can now use `.env` files to mount secrets. See the [Deployment](/docs/deployment) guide.
- One login can now access multiple orgs.

#### Bug Fixes & Improvements

- Increased robustness of ASR background noise rejection and short utterance handling.
- Various ease-of-use updates to the [Guava CLI](/docs/cli-reference).

---

### April 29, 2026

The Guava SIP Trunk goes live, bringing inbound SIP calling support. Agent capabilities and campaign management see significant additions.

#### New Features

- The **Guava SIP Trunk** is now live, enabling agents to receive inbound calls over SIP. See the [SIP Integration](/docs/sip-integrations) guide.
- Voicemail behavior is now configurable via [`set_voicemail_action()`](/docs/set-voicemail-action).
- The `guava widget` CLI command generates embed codes for [WebRTC-based voice widget](/docs/webrtc-widgets) integrations.
- `agent.on_escalate` enables agents to hand off calls to another destination. See [`transfer()`](/docs/transfer) for related functionality.
- [`set_language_mode()`](/docs/set-language-mode) has been added to allow language configuration.
- The [Campaigns API](/docs/campaign) now supports streamlined outbound campaign creation and modification.

---
