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)


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