# Customer Support Agent

A support agent answers the questions your support team answers every day, at any hour, and hands the rest over to your team.

>
> **Note:** **What you'll build**
>
> - A support agent that answers from your help center and stays on topic
> - Order lookups and refunds on your backend, with a question to the user before a refund
> - A ticket for your team when the agent can't help
> - A help screen that carries on the user's latest support conversation, with ratings and a button that calls support
>

**Starts from:** the [Quickstart](https://getstream.io/chat/docs/sdk/ios/ai-components/), with `agents`, `AgentMessage` and `AgentTransport` in your project.

## Best practices

- Give the agent your help center as knowledge, so it answers from what you published rather than from what the model guesses.
- Run the tools that reach your systems, such as orders and refunds, on your backend, not on the phone.
- Ask before anything that can't be undone, such as a refund.
- Always offer a way to reach a person.

## Define the support agent

Make the agent from the text template:

```sh
getstream agents init support --template text
```

Its instructions say what it's for:

```md title="agents/support/instructions.md"
You are Acme's support assistant. Answer questions about Acme orders, shipping,
returns and billing, briefly.

- Look up an order before you answer a question about it.
- When you can't help, or the customer asks for a person, open a ticket.
```

Its knowledge is your help center, and its guardrail turns away what isn't about Acme:

```yaml title="agents/support/knowledge/urls.yaml"
- https://help.acme.com/shipping
- https://help.acme.com/returns
```

```md title="agents/support/guardrail.md"
---
refusal: I can only help with questions about your Acme account and orders.
---

Answer questions about Acme orders, billing, shipping and returns. Refuse anything else.
```

Sync it, and try it in the terminal:

```sh
getstream agents test support --sync
```

[Knowledge & Skills](https://getstream.io/agents/docs/knowledge-and-skills/?lang=ios) and [Guardrails](https://getstream.io/agents/docs/guardrails/?lang=ios) cover both in full.

## Host its tools on your backend

The tools that reach your systems run on your backend, in a [dispatch worker](https://getstream.io/agents/docs/tool-calling-and-dispatch/?lang=ios#hosting-tools). Each call knows the user it's for, so a user only finds their own orders. `issue_refund` declares an approval, so it runs only once the user allows it:

```ts label="TypeScript" title="worker.ts"
import { Client, Dispatch } from "@stream-io/stream-agents";
import { z } from "zod";

const agent = new Client().agent("support");
const order = z.object({ order_id: z.string() });

agent.tools.register({
  name: "lookup_order",
  description: "Look up one of the customer's orders",
  parameters: order,
  run: ({ order_id }, signal, { user }) => orders.find(order_id, user.id),
});

agent.tools.register({
  name: "issue_refund",
  description: "Refund an order",
  parameters: order,
  approval: { title: "Refund order {order_id}?" },
  run: ({ order_id }, signal, { user }) => refunds.issue(order_id, user.id),
});

agent.tools.register({
  name: "open_ticket",
  description: "Hand the conversation over to the support team",
  parameters: z.object({ summary: z.string() }),
  run: ({ summary }, signal, { user }) => helpdesk.open(user.id, summary),
});

await new Dispatch().host(agent).run();
```

`orders`, `refunds` and `helpdesk` stand for your own systems. [Tool calling & Dispatch](https://getstream.io/agents/docs/tool-calling-and-dispatch/?lang=ios) shows the same worker in other languages.

## Open the user's support conversation

Carry on the user's latest support session, so the agent has the history, or start one:

```swift
import StreamAgents

@MainActor
func openSupport() async throws -> AgentTransport {
    let latest = try await agents.agent("support").sessions.query(SessionQuery(limit: 1)).items.first
    // A session that was only a call has no written conversation.
    if let latest, !latest.conversationID.isEmpty {
        return try await AgentTransport(agents.attach(sessionID: latest.id))
    }
    return try await AgentTransport(agents.chat(agent: "support"))
}
```

## Build the support screen

The support screen is the quickstart's conversation with common questions while it's empty, and a button that calls support. Its `actions` keep rating, and leave out editing and regenerating:

```swift
import StreamChatAI
import SwiftUI

struct SupportView: View {
    @StateObject private var conversation: AIConversation<AgentMessage>
    @State private var isCalling = false

    init(transport: AgentTransport) {
        _conversation = StateObject(
            wrappedValue: AIConversation(transport: transport, approver: transport.approver, actions: [.rate])
        )
    }

    var body: some View {
        AIConversationView(conversation: conversation, emptyContent: {
            SuggestionsView(suggestions: ["Where is my order?", "I want to return an item", "Talk to a person"]) {
                conversation.send($0)
            }
        })
        .safeAreaInset(edge: .bottom) {
            AIComposerView(conversation: conversation)
        }
        .toolbar {
            Button("Call support", systemImage: "phone.fill") { isCalling = true }
        }
        .fullScreenCover(isPresented: $isCalling) {
            VoiceAgentView(agent: "support")
        }
    }
}
```

Open it the way the quickstart's `AssistantScreen` does, with `openSupport()` in place of `agents.chat`. `VoiceAgentView` is the call screen of the [Voice agent](https://getstream.io/chat/docs/sdk/ios/ai-components/use-cases/voice-agent/) use case, and talks to the same agent, with the same knowledge and tools.

Everything else comes from the agent, with nothing more to add in the app:

- **Order lookups** show in the reply as the agent makes them, so the user sees what it checked.
- **A refund** shows "Refund order 1042?" under its call, with buttons to allow or decline it. Declined, the refund never runs, and the agent is told so.
- **A handover** opens a ticket with the agent's summary, so nobody asks the user to start again. The [Slack connector](https://getstream.io/agents/docs/connectors/slack/?lang=ios) lets your team follow along.

## Send ratings to your backend

Ratings stay on the device unless the transport sends them. Add a `rate(_:_:)` to `AgentTransport`:

```swift
import StreamChatAI

extension AgentTransport {
    func rate(_ reply: AgentMessage, _ rating: AIMessageRating?) async throws {
        try await yourBackend.rate(sessionID: session.id, messageID: reply.id, rating: rating)
    }
}
```

The router also reviews and scores each session once it ends, which [Sessions](https://getstream.io/agents/docs/sessions/?lang=ios#title-description-and-review) describes.

## Next steps

- [Voice agent](https://getstream.io/chat/docs/sdk/ios/ai-components/use-cases/voice-agent/): the call screen the Call support button opens.
- [Purchase a phone number](https://getstream.io/agents/docs/telephony/purchase-number/?lang=ios) and [Transfer to a person](https://getstream.io/agents/docs/telephony/transfer/?lang=ios): the same agent on the phone.
- [Tool calling & Dispatch](https://getstream.io/agents/docs/tool-calling-and-dispatch/?lang=ios): tools, approvals, timeouts and progress updates.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/ios/ai-components/use-cases/customer-support-agent/](https://getstream.io/chat/docs/sdk/ios/ai-components/use-cases/customer-support-agent/).