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
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 poetryAlso install boto3 for the Amazon Connect task API:
pip install boto3Step 2: Set Up Amazon Connect
2a. Create or locate your Connect instance
- Open the Amazon Connect console.
- Create an instance if you don't have one, or select an existing one.
- 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.
- In your Connect instance, go to Routing → Contact flows → Create contact flow.
- Name it
Guava Task Flow(or similar). - Add a Set working queue block and point it to your support queue.
- Connect it to a Transfer to queue block, then a Disconnect block.
- Save and publish the flow.
- 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.
- Go to Routing → Phone numbers in your Connect instance.
- Use any number claimed to your instance that routes to your support queue, or claim a new one.
- 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.
-
Go to Routing → Contact flows → Create contact flow (type: Inbound contact flow).
-
Name it
AI Customer Service. -
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 transferorPSTN.
- Set Phone number to your Guava agent number (the value of
- Add a Disconnect block on the error branch.
-
Save and publish the flow.
-
Assign this flow to an inbound phone number: go to Channels → Phone numbers, select a number, and set its contact flow to
AI Customer Service.
Adding routing logic: If you want Connect to handle an IVR before routing to Riley, add a Get customer input block before the transfer. You can route product questions to Guava and billing escalations to a human queue based on the customer's menu selection.
Step 3: Configure AWS Credentials
export AWS_ACCESS_KEY_ID="your-access-key-id"
export AWS_SECRET_ACCESS_KEY="your-secret-access-key"
export AWS_DEFAULT_REGION="us-east-1" # Match your Connect instance regionThe IAM user or role needs:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["connect:StartTaskContact"],
"Resource": "arn:aws:connect:*:*:instance/YOUR_INSTANCE_ID/*"
}
]
}Step 4: Set Environment Variables
# Guava
export GUAVA_API_KEY="your-guava-api-key"
export GUAVA_AGENT_NUMBER="+15551000000" # Your Guava phone number (Riley's number)
# Amazon Connect
export CONNECT_INSTANCE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export CONNECT_CONTACT_FLOW_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # Task flow from Step 2b
export CONNECT_SUPPORT_QUEUE_NUMBER="+15551234567" # Live agent queue from Step 2cStep 5: Run Riley
python -m examples.integrations.ccaas.amazon_connect.ai_customer_serviceYou should see:
INFO:ai_customer_service:Riley is ready — listening for inbound calls on +15551000000Riley is now live. Call your Amazon Connect number (the one you assigned the AI Customer Service flow to in Step 2d) — Connect will transfer the call to Riley automatically.
How the Code Works
Defining the agent
agent = Agent(
name="Riley",
organization="Pinnacle Gear Co.",
purpose="to help Pinnacle Gear customers with product questions, orders, returns, and warranty support",
)The Agent is a top-level handle for your AI persona. You attach behavior to it with decorators (@agent.on_call_start, @agent.on_question, @agent.on_task_complete(...)) — each decorator registers a handler that runs when the corresponding event fires on a live call.
Accepting inbound calls
agent.inbound_phone(os.environ["GUAVA_AGENT_NUMBER"]).run()inbound_phone opens a persistent WebSocket to the Guava server and waits for calls. Each time a call arrives on Riley's number — whether from a customer dialing directly or transferred from Amazon Connect — the agent's registered handlers run on a fresh Call object that represents that conversation.
Kicking off the conversation
@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
call.set_task(
"intake",
objective="...",
checklist=[
guava.Say("Thanks for calling Pinnacle Gear. I'm Riley, and I'm here to help. ..."),
guava.Field(key="customer_name", ...),
guava.Field(key="inquiry", ...),
],
)on_call_start runs as soon as the call connects. We use it to set the first task — a named intake step the agent works through, greeting the caller and collecting the fields we need before deciding what to do next.
Answering questions with a knowledge base
@agent.on_question
def on_question(call: guava.Call, question: str) -> str:
return document_qa.ask(question)Whenever a caller asks something Riley can't answer from context alone — "What's your return policy?", "Is this jacket machine washable?" — Guava invokes on_question with the question in natural language. DocumentQA retrieves the most relevant chunks from SUPPORT_DOCS and generates an accurate answer. Riley speaks the answer back and continues the conversation without any perceivable delay.
DocumentQA() with no store argument runs in server mode — your documents are uploaded to the Guava server and searched there. The namespace parameter scopes the uploaded documents to this agent so they don't collide with documents from other DocumentQA instances in the same account.
Routing after the conversation
intent_recognizer = IntentRecognizer(
{
"live_agent": "Caller wants to speak with a human, agent, manager, or supervisor, or has a complaint, legal threat, or sensitive escalation",
"follow_up_task": "Caller needs a return, refund, exchange, warranty claim, or has a damaged or wrong item that requires team follow-up",
"resolved": "Caller's question was answered and no further action is needed",
}
)
@agent.on_task_complete("intake")
def handle_inquiry(call: guava.Call) -> None:
inquiry = call.get_field("inquiry", "")
intent = intent_recognizer.classify(inquiry)
if intent == "live_agent":
escalate_to_human(call)
elif intent == "follow_up_task":
collect_followup_details(call)
else:
call.hangup(...)After the intake task finishes collecting details, the registered @agent.on_task_complete("intake") handler classifies the outcome using IntentRecognizer and routes accordingly. Using an LLM-based classifier handles natural phrasing correctly — "the zipper broke on the first use" maps to follow_up_task even without explicitly matching "broken". The recognizer is instantiated once at module level and reused across calls.
| Outcome | Example signals | Action |
|---|---|---|
| Resolved | Question answered, no action needed | Wrap up and end call |
| Follow-up | Return request, warranty claim | Collect email, create Connect task |
| Live agent | "I want to speak to someone", complaint | Transfer back to Connect |
Transferring back to Amazon Connect
def escalate_to_human(call: guava.Call) -> None:
call.transfer(
os.environ["CONNECT_SUPPORT_QUEUE_NUMBER"],
"Let the customer know you're connecting them with a specialist. "
"Then transfer the call.",
)The second argument to call.transfer gives Riley natural language guidance on what to say before transferring — she'll let the customer know what's happening and transfer when it feels right in the conversation. The customer lands in the Connect support queue as a normal inbound call, which agents handle in the Contact Control Panel (CCP).
Creating a Connect task for follow-up
connect_client.start_task_contact(
InstanceId=os.environ["CONNECT_INSTANCE_ID"],
ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
Name=f"Follow-up Required — {call.get_field('customer_name')}"[:512],
Description=f"Customer: ...\nEmail: ...\nOrder: ...\nIssue: ..."[:4096],
Attributes={...},
)When a caller needs follow-up but doesn't need a live agent right now, Riley collects their email and order number in a second task (follow_up). The @agent.on_task_complete("follow_up") handler then creates a task in Amazon Connect via StartTaskContact. Agents see it in the CCP like any other contact — with the customer's name, issue summary, and email for follow-up.
Customization Ideas
Use your own knowledge base
Replace SUPPORT_DOCS with your actual product documentation, policy pages, or FAQs. DocumentQA handles chunking and retrieval automatically — paste in as much text as you need.
Add IVR routing in Connect before transferring Instead of routing all calls to Riley, add a Get customer input block in your Connect flow. Route product questions to Riley's number and billing escalations to a human queue. Riley only handles the calls she's best suited for.
Pass context from Connect to Riley via SIP
For more advanced setups, Amazon Connect can transfer calls over a SIP trunk to a Guava SIP endpoint. This lets Connect pass the original Contact ID and other attributes as SIP headers, which the agent can read from the call info. You can then link Riley's Connect task back to the original call using PreviousContactId for full call chain reporting.
Route different inquiry types to different agents Run multiple Guava agents on different agent numbers — one for product support, one for returns, one for technical help. Have Connect's IVR route to the right one based on the customer's menu selection.
Use call info to customize behavior per call
The @agent.on_call_received decorator lets you inspect the inbound CallInfo (caller number, SIP headers, etc.) before the call is accepted. Use this to accept or decline calls, or to set per-call variables in on_call_start that change Riley's behavior — for example, routing region-specific callers to region-specific transfer numbers.
Complete Example
import guava
import os
import logging
import json
import boto3
from datetime import datetime
from guava import Agent, logging_utils
from guava.helpers.openai import IntentRecognizer
from guava.helpers.rag import DocumentQA
logger = logging.getLogger("ai_customer_service")
# ---------------------------------------------------------------------------
# Knowledge base
# Guava answers caller questions from these documents in real time.
# Replace with your own product docs, policies, or FAQs.
# ---------------------------------------------------------------------------
SUPPORT_DOCS = """
Pinnacle Gear Co. — Customer Support Reference
RETURNS & REFUNDS
- We accept returns within 30 days of purchase for unused items in original packaging.
- Items showing signs of use may be exchanged for store credit at our discretion.
- Refunds are processed within 5–7 business days of receiving the returned item.
- Sale items are final sale and cannot be returned or exchanged.
- Start a return at pinnaclegear.com/returns or by calling our support line.
SHIPPING
- Standard shipping (5–7 business days): free on orders over $75, otherwise $7.99.
- Expedited shipping (2–3 business days): $14.99 flat rate.
- Overnight shipping: $29.99. Orders placed before 2 PM ET ship the same day.
- We ship to all 50 US states and Canada. International shipping is not available.
WARRANTY
- All products carry a 1-year limited warranty against manufacturing defects.
- Summit Series backpacks and Trail Pro footwear carry a lifetime warranty.
- Warranty claims require proof of purchase. Email support@pinnaclegear.com.
- Damage from misuse, normal wear, or accidents is not covered.
SIZING
- Apparel follows standard US sizing. See pinnaclegear.com/size-guide.
- Footwear runs true to size; for wide feet, size up by half a size.
- When between sizes, size up for layering or down for an athletic fit.
ORDERS & ACCOUNT
- Track orders at pinnaclegear.com/track using your order number and email.
- To modify or cancel an order, contact us within 1 hour of placing it.
- We accept Visa, Mastercard, Amex, Discover, PayPal, and Pinnacle gift cards.
- Pinnacle Rewards: 1 point per dollar spent; 100 points = $5 reward credit.
PRODUCT CARE
- Machine wash apparel on cold, gentle cycle. Tumble dry on low heat.
- Do not use fabric softener on moisture-wicking or DWR-coated gear.
- Re-apply DWR treatment after 10–15 wash cycles.
- Store sleeping bags loosely in a large cotton sack, never compressed long-term.
"""
document_qa = DocumentQA(documents=SUPPORT_DOCS, namespace="pinnacle-gear-customer-service")
connect_client = boto3.client("connect")
intent_recognizer = IntentRecognizer(
{
"live_agent": "Caller wants to speak with a human, agent, manager, or supervisor, or has a complaint, legal threat, or sensitive escalation",
"follow_up_task": "Caller needs a return, refund, exchange, warranty claim, or has a damaged or wrong item that requires team follow-up",
"resolved": "Caller's question was answered and no further action is needed",
}
)
agent = Agent(
name="Riley",
organization="Pinnacle Gear Co.",
purpose=(
"to help Pinnacle Gear customers with product questions, orders, "
"returns, and warranty support"
),
)
@agent.on_call_start
def on_call_start(call: guava.Call) -> None:
call.set_task(
"intake",
objective=(
"Help the customer with their inquiry. Answer questions accurately using "
"the knowledge base. If their issue needs a human specialist or involves "
"a complaint or legal matter, let them know you'll connect them with someone."
),
checklist=[
guava.Say(
"Thanks for calling Pinnacle Gear. I'm Riley, and I'm here to help."
),
guava.Field(
key="customer_name",
description="Ask for the customer's name.",
field_type="text",
required=True,
),
guava.Field(
key="inquiry",
description=(
"Understand what the customer needs. Answer their question if you can "
"using the knowledge base. If you cannot resolve it — they want a "
"return, refund, complaint resolution, or to speak with a person — "
"capture what they need in detail."
),
field_type="text",
required=True,
),
],
)
@agent.on_question
def on_question(call: guava.Call, question: str) -> str:
"""Answer caller questions from the knowledge base in real time."""
return document_qa.ask(question)
@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(
"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()Questions? hi@goguava.ai
pip install guava-sdk # Install using pip
uv add guava-sdk # Install using uv
poetry add guava-sdk # Install using poetryexport 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{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["connect:StartTaskContact"],
"Resource": "arn:aws:connect:*:*:instance/YOUR_INSTANCE_ID/*"
}
]
}# 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 2cagent = 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="...",
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", ...),
],
)@agent.on_question
def on_question(call: guava.Call, question: str) -> str:
return document_qa.ask(question)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(...)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.",
)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={...},
)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)
@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(
"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()