docs/Product Support

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

Prerequisites

  • Python 3.10 or later
  • A Guava account with an API key and a phone number — sign up at app.goguava.ai
  • An AWS account with an Amazon Connect instance

Step 1: Install Guava

Choose whichever package manager you prefer:

pip install guava-sdk    # Install using pip
uv add guava-sdk         # Install using uv
poetry add guava-sdk     # Install using poetry

You also need boto3 for the Amazon Connect API:

pip install boto3

Step 2: Set Up Amazon Connect

2a. Create or locate your Connect instance

  1. Open the Amazon Connect console.
  2. Create an instance if you don't have one, or select an existing one.
  3. Note the Instance ID — it's the UUID at the end of the instance ARN:
    arn:aws:connect:us-east-1:123456789012:instance/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                                                    └─────────────── Instance ID ──────┘

2b. Create a 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."
    • 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 Product Support.

No live transfer queue here. Unlike the AI Customer Service example, this example doesn't transfer escalations back to a live agent — it creates an asynchronous Connect task instead. If you'd rather offer a live transfer for escalations, see the AI Customer Service walkthrough for that pattern.

Step 3: Configure AWS Credentials

export AWS_ACCESS_KEY_ID="your-access-key-id"
export AWS_SECRET_ACCESS_KEY="your-secret-access-key"
export AWS_DEFAULT_REGION="us-east-1"   # Match your Connect instance region

The IAM user or role needs:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["connect:StartTaskContact"],
      "Resource": "arn:aws:connect:*:*:instance/YOUR_INSTANCE_ID/*"
    }
  ]
}

Step 4: Set Environment Variables

# Guava
export GUAVA_API_KEY="your-guava-api-key"
export GUAVA_AGENT_NUMBER="+15551000000"      # Your Guava phone number (Jordan's number)

# Amazon Connect
export CONNECT_INSTANCE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export CONNECT_CONTACT_FLOW_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"   # Task flow from Step 2b

Step 5: Run Jordan

python -m examples.integrations.ccaas.amazon_connect.product_support

You should see:

INFO:product_support:Jordan is ready — listening for inbound calls on +15551000000

Jordan is now live. Call your Amazon Connect number (the one you assigned the Product Support flow to in Step 2c) — Connect will transfer the call to Jordan automatically.

What to expect:

  • Jordan greets the caller, takes their name, and asks what they need
  • For straightforward questions ("How do I wash my jacket?", "What's your return window?", "Do you ship to Canada?"), Jordan answers from the product FAQ in real time and wraps up the call
  • For requests that need a human (returns, refunds, complaints, damaged items, "can I speak to a manager"), Jordan collects the customer's email and order number, creates a Connect task with the issue summary, and tells the customer a specialist will email them within one business day

How the Code Works

The product FAQ knowledge base

# knowledge_base.py
PRODUCT_FAQ = """
Pinnacle Gear Co. — Product FAQ

RETURNS & REFUNDS
- We accept returns within 30 days of purchase ...

SHIPPING
- Standard shipping (5–7 business days): free on orders over $75 ...

WARRANTY
- All Pinnacle Gear products carry a 1-year limited warranty ...
...
"""

The knowledge base lives in a sibling knowledge_base.py file as a single multi-line string. Keeping it in its own module makes it easy to swap in a longer corpus, edit copy without touching the agent code, or load from a file at startup. There's no chunking or indexing required — DocumentQA handles all of that.

Wiring up the agent

from knowledge_base import PRODUCT_FAQ

document_qa = DocumentQA(documents=PRODUCT_FAQ, namespace="pinnacle-gear-product-support")

agent = Agent(
    name="Jordan",
    organization="Pinnacle Gear Co.",
    purpose=(
        "to help customers with product questions, returns, shipping, "
        "and warranty inquiries"
    ),
)

Agent is the top-level handle for the AI persona. DocumentQA with no store argument runs in server mode — your documents are uploaded to the Guava server and searched there, so you don't need to set up GCP credentials or a vector store yourself. The namespace parameter scopes the upload to this agent so it doesn't collide with documents from other DocumentQA instances in the same account.

DocumentQA is content-addressed: if PRODUCT_FAQ hasn't changed since the last run, nothing is re-uploaded — startup is instant after the first launch.

Accepting inbound calls

agent.inbound_phone(os.environ["GUAVA_AGENT_NUMBER"]).run()

inbound_phone opens a persistent WebSocket to the Guava server and waits for calls. Each time a call arrives on Jordan's number — whether dialed directly or transferred from the Amazon Connect contact flow — the agent's registered handlers run on a fresh Call object that represents that conversation.

Kicking off the conversation

@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
    call.set_task(
        "intake",
        objective="Help the customer with their Pinnacle Gear question. ...",
        checklist=[
            guava.Say("Thank you for calling Pinnacle Gear. My name is Jordan. ..."),
            guava.Field(key="customer_name", ...),
            guava.Field(key="issue_summary", ...),
        ],
    )

on_call_start runs as soon as the call connects. We use it to set the first task — a named intake step Jordan works through, greeting the caller, taking their name, and capturing what they need in their own words. The issue_summary field's description tells Jordan to try to resolve the question first using the knowledge base, and only escalate if she can't — so simple FAQ questions get answered without ever entering an escalation flow.

Answering questions with a knowledge base

@agent.on_question
def on_question(call: guava.Call, question: str) -> str:
    """Answer product questions from the knowledge base in real time."""
    return document_qa.ask(question)

Whenever a caller asks something Jordan can't answer from context alone — "What's your return policy?", "Is this jacket machine washable?", "Do you ship overnight?" — Guava invokes on_question with the question in natural language. DocumentQA.ask retrieves the most relevant chunks from the FAQ and generates an accurate answer. Jordan speaks the answer back and continues the conversation without any perceivable delay.

This decorator is what gives Jordan her FAQ superpowers: she'll never make up a return policy. If the FAQ doesn't cover the question, the answer will reflect that, and the customer naturally drops into the escalation path.

Routing after the conversation

intent_recognizer = IntentRecognizer(
    {
        "escalation_needed": "Customer needs a return, refund, exchange, or complaint resolved, wants to speak with a manager or specialist, or has a damaged or broken item",
        "resolved": "Customer's question was answered and no further action is needed",
    }
)

@agent.on_task_complete("intake")
def handle_outcome(call: guava.Call) -> None:
    issue = call.get_field("issue_summary", "")
    if intent_recognizer.classify(issue) == "escalation_needed":
        collect_escalation_details(call)
    else:
        call.hangup("Thank the customer for calling Pinnacle Gear. ...")

After the intake task finishes, the registered @agent.on_task_complete("intake") handler classifies the outcome using IntentRecognizer and routes accordingly. Using an LLM-based classifier handles natural phrasing correctly — "the seam ripped after one trip" maps to escalation_needed even without explicitly matching "broken" or "return". The recognizer is instantiated once at module level and reused across calls.

OutcomeExample signalsAction
ResolvedFAQ question answered, no action neededWrap up and end call
Escalation neededReturn, refund, complaint, damaged item, manager requestCollect email + order number, create Connect task

Collecting follow-up details

def collect_escalation_details(call: guava.Call) -> None:
    call.set_task(
        "escalation",
        objective="Collect the customer's contact details so a specialist can follow up.",
        checklist=[
            guava.Say("I'll have a specialist follow up with you directly to take care of that."),
            guava.Field(key="email", ..., required=True),
            guava.Field(key="order_number", ..., required=False),
        ],
    )

If the issue requires escalation, Jordan starts a second task to collect the email and order number. The order number is required=False because not every issue is order-bound (warranty inquiries on long-owned products, general complaints), and the field description explicitly tells Jordan that "if they don't have one, that's fine" — so she doesn't get stuck pestering the customer for something that doesn't exist.

Creating a Connect task for follow-up

@agent.on_task_complete("escalation")
def create_support_task(call: guava.Call) -> None:
    ...
    connect_client.start_task_contact(
        InstanceId=os.environ["CONNECT_INSTANCE_ID"],
        ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
        Name=f"Support Escalation — {call.get_field('customer_name', 'Customer')}"[:512],
        Description=f"Customer: ...\nEmail: ...\nOrder: ...\nIssue: ..."[:4096],
        Attributes={
            "customer_name": call.get_field("customer_name", ""),
            "email": call.get_field("email", ""),
            "order_number": call.get_field("order_number", ""),
        },
    )
    call.hangup("Let the customer know a specialist will reach out by email ...")

Once the escalation task completes, the @agent.on_task_complete("escalation") handler creates a task in Amazon Connect via StartTaskContact. The task's Description carries the full issue summary, and its Attributes make the customer's name, email, and order number searchable in Connect's contact search — so support agents can pick up the work in the Contact Control Panel just like any other contact, with full context.

The handler also prints the captured fields as JSON for local debugging and ends the call with a promise of email follow-up within one business day.


Customization Ideas

Use your own product documentation Replace PRODUCT_FAQ with your real product manuals, policy pages, or help center articles. DocumentQA handles chunking and retrieval automatically — paste in as much text as you need, or load from files at startup:

faq_text = pathlib.Path("docs/product_faq.md").read_text()
document_qa = DocumentQA(documents=faq_text, namespace="my-product-faq")

Combine with a live-transfer escalation path This example only creates async tasks. If some escalations need a live agent immediately (high-value customers, urgent complaints), add a third intent class — e.g. live_agent — and use call.transfer(...) to send those calls back to a Connect queue, like the AI Customer Service example does.

Look up customer info before answering If you can identify the caller from their phone number (CRM lookup), pass their order history into the call as variables and reference it from on_call_start. Jordan can then say "I see your recent order shipped on Tuesday" without the customer needing to provide an order number.

Add real-time inventory or order lookups The @agent.on_question decorator can call any code, not just DocumentQA. Inspect the question, route product-availability questions to your inventory API, route order-status questions to your OMS, and fall back to the FAQ for everything else.

Pass context from Connect to Jordan via SIP For more advanced setups, Amazon Connect can transfer calls over a SIP trunk to a Guava SIP endpoint. This lets Connect pass the original Contact ID and other attributes as SIP headers, which the agent can read from the call info — and you can link the resulting escalation task back to the original call using RelatedContactId for full call chain reporting (see the CSAT Survey example for the same RelatedContactId pattern).


Complete Example

__main__.py

import guava
import os
import sys
import logging
import json
import pathlib
import boto3
from datetime import datetime

from guava import Agent, logging_utils
from guava.helpers.openai import IntentRecognizer
from guava.helpers.rag import DocumentQA

logger = logging.getLogger("product_support")

# Load the product FAQ from the sibling knowledge_base module.
sys.path.insert(0, str(pathlib.Path(__file__).parent))
from knowledge_base import PRODUCT_FAQ

# Initialize DocumentQA in server mode — no GCP credentials needed.
# Documents are content-addressed: unchanged FAQ text is never re-uploaded.
document_qa = DocumentQA(documents=PRODUCT_FAQ, namespace="pinnacle-gear-product-support")

connect_client = boto3.client("connect")

intent_recognizer = IntentRecognizer(
    {
        "escalation_needed": "Customer needs a return, refund, exchange, or complaint resolved, wants to speak with a manager or specialist, or has a damaged or broken item",
        "resolved": "Customer's question was answered and no further action is needed",
    }
)

agent = Agent(
    name="Jordan",
    organization="Pinnacle Gear Co.",
    purpose=(
        "to help customers with product questions, returns, shipping, "
        "and warranty inquiries"
    ),
)


@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
    call.set_task(
        "intake",
        objective=(
            "Help the customer with their Pinnacle Gear question. Use the knowledge base "
            "to answer product questions accurately. If the customer needs a return, refund, "
            "or to speak with a specialist, collect their details for follow-up."
        ),
        checklist=[
            guava.Say(
                "Thank you for calling Pinnacle Gear. My name is Jordan. "
                "I can help with product questions, returns, shipping, and warranty. "
                "How can I help you today?"
            ),
            guava.Field(
                key="customer_name",
                description="Ask for the customer's name.",
                field_type="text",
                required=True,
            ),
            guava.Field(
                key="issue_summary",
                description=(
                    "Understand what the customer needs. Answer their question if you can "
                    "using the knowledge base. If you can't resolve it — return request, "
                    "refund, complaint, or request to speak with a specialist — note what "
                    "they need in detail."
                ),
                field_type="text",
                required=True,
            ),
        ],
    )


@agent.on_question
def on_question(call: guava.Call, question: str) -> str:
    """Answer product questions from the knowledge base in real time."""
    return document_qa.ask(question)


@agent.on_task_complete("intake")
def handle_outcome(call: guava.Call) -> None:
    issue = call.get_field("issue_summary", "")
    if intent_recognizer.classify(issue) == "escalation_needed":
        collect_escalation_details(call)
    else:
        call.hangup(
            "Thank the customer for calling Pinnacle Gear. Ask if there's anything "
            "else you can help with. If not, wish them a great day."
        )


def collect_escalation_details(call: guava.Call) -> None:
    call.set_task(
        "escalation",
        objective="Collect the customer's contact details so a specialist can follow up.",
        checklist=[
            guava.Say(
                "I'll have a specialist follow up with you directly to take care of that."
            ),
            guava.Field(
                key="email",
                description="Ask for the customer's email address.",
                field_type="text",
                required=True,
            ),
            guava.Field(
                key="order_number",
                description=(
                    "Ask for their order number if relevant to the issue. "
                    "If they don't have one, that's fine."
                ),
                field_type="text",
                required=False,
            ),
        ],
    )


@agent.on_task_complete("escalation")
def create_support_task(call: guava.Call) -> None:
    results = {
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "agent": "Jordan",
        "organization": "Pinnacle Gear Co.",
        "use_case": "inbound_product_support",
        "fields": {
            "customer_name": call.get_field("customer_name"),
            "issue_summary": call.get_field("issue_summary"),
            "email": call.get_field("email"),
            "order_number": call.get_field("order_number"),
        },
    }
    print(json.dumps(results, indent=2))
    logger.info("Escalation details captured.")

    try:
        connect_client.start_task_contact(
            InstanceId=os.environ["CONNECT_INSTANCE_ID"],
            ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
            Name=f"Support Escalation — {call.get_field('customer_name', 'Customer')}"[:512],
            Description=(
                f"Customer: {call.get_field('customer_name')}\n"
                f"Email: {call.get_field('email')}\n"
                f"Order: {call.get_field('order_number', 'N/A')}\n"
                f"Issue: {call.get_field('issue_summary')}"
            )[:4096],
            References={
                "source": {"Value": "guava_product_support", "Type": "STRING"},
            },
            Attributes={
                "customer_name": call.get_field("customer_name", ""),
                "email": call.get_field("email", ""),
                "order_number": call.get_field("order_number", ""),
            },
        )
        logger.info("Amazon Connect support task created successfully.")
    except Exception as e:
        logger.error("Failed to create Amazon Connect task: %s", e)

    call.hangup(
        "Let the customer know a specialist will reach out by email within one business "
        "day. Thank them for their patience and wish them a great day."
    )


if __name__ == "__main__":
    logging_utils.configure_logging()
    agent.inbound_phone(os.environ["GUAVA_AGENT_NUMBER"]).run()

knowledge_base.py

This file contains the product FAQ document imported by the main script.

PRODUCT_FAQ = """
Pinnacle Gear Co. — Product FAQ

RETURNS & REFUNDS
- We accept returns within 30 days of purchase for unused items in original packaging.
- Items showing signs of use may be returned for store credit only, at our discretion.
- To start a return, visit pinnaclegear.com/returns or call our support line.
- Refunds are processed within 5–7 business days after we receive the returned item.
- Sale items are final sale and cannot be returned or exchanged.
- Gift recipients can exchange items for equal or lesser value without a receipt.

SHIPPING
- Standard shipping (5–7 business days): free on orders over $75, otherwise $7.99.
- Expedited shipping (2–3 business days): $14.99 flat rate.
- Overnight shipping (next business day): $29.99. Orders placed before 2 PM ET ship same day.
- We ship to all 50 US states and Canada. International shipping is not yet available.
- Orders placed before 2 PM ET on business days ship the same day.

WARRANTY
- All Pinnacle Gear products carry a 1-year limited warranty against manufacturing defects.
- Our Summit Series backpacks and Trail Pro footwear carry a lifetime warranty.
- Warranty claims require proof of purchase. Contact support@pinnaclegear.com to file a claim.
- Normal wear and tear, damage from misuse, or accidental damage are not covered under warranty.

SIZING
- Apparel follows standard US sizing. See our full size guide at pinnaclegear.com/size-guide.
- Footwear runs true to size. For wide feet, we recommend sizing up by half a size.
- Backpack sizing is based on torso length, not height. Measure from your C7 vertebra to your iliac crest.
- When between sizes in apparel, size up for layering and size down for a fitted athletic look.

PRODUCT CARE
- Most Pinnacle apparel is machine washable on cold, gentle cycle. Tumble dry on low heat.
- Do not use fabric softener on moisture-wicking or DWR-coated items — it reduces performance.
- Down insulation should be washed on a delicate cycle and dried on low heat with clean tennis balls.
- Re-apply DWR water repellent treatment (such as Nikwax TX.Direct) after 10–15 wash cycles.
- Store sleeping bags loosely in a large cotton storage sack — never compressed for long periods.
- Clean tents with mild soap and cool water. Never machine wash or put a tent in the dryer.

ORDERS & ACCOUNT
- Track your order at pinnaclegear.com/track using your order number and the email on the order.
- To modify or cancel an order, contact us within 1 hour of placing it. After that, it may have shipped.
- We accept Visa, Mastercard, American Express, Discover, PayPal, and Pinnacle gift cards.
- Pinnacle Rewards members earn 1 point per dollar spent. 100 points = $5 reward credit.
- To create an account or reset your password, visit pinnaclegear.com/account.
"""

Questions? hi@goguava.ai