# 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/android/ai-components/), with `agents`, `AgentChat` and `AgentMessage` 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=android) and [Guardrails](https://getstream.io/agents/docs/guardrails/?lang=android) 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=android#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=android) 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:

```kotlin
import io.getstream.agents.android.core.AgentSession
import io.getstream.agents.android.core.SessionQuery

suspend fun openSupport(): AgentSession {
    val support = agents.agent("support")
    val latest = support.sessions.query(SessionQuery(limit = 1)).items.firstOrNull()
    // A session that was only a call has no written conversation.
    if (latest != null && latest.conversationId.isNotEmpty()) {
        return agents.attach(latest.id)
    }
    return support.chat()
}
```

## 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. Open its chat the way the quickstart's `AssistantViewModel` does, with `openSupport()` in place of `agents.agent("my-agent").chat()`:

```kotlin
class SupportViewModel(application: Application) : AndroidViewModel(application) {
    private val _chat = MutableStateFlow<AgentChat?>(null)
    val chat: StateFlow<AgentChat?> = _chat.asStateFlow()

    init {
        viewModelScope.launch {
            _chat.value = AgentChat.open(application, openSupport(), viewModelScope)
        }
    }
}
```

The reply actions under each answer keep rating, and leave out regenerating:

```kotlin
import androidx.compose.runtime.Composable
import io.getstream.chat.android.ai.compose.conversation.AIMessageRating
import io.getstream.chat.android.ai.compose.conversation.AIReplyActions
import io.getstream.chat.android.ai.compose.ui.component.AIConversationReplyActionsParams
import io.getstream.chat.android.ai.compose.ui.component.AIReplyActionsRow
import io.getstream.chat.android.ai.compose.ui.component.ChatAiComponentFactory

class SupportReplyActions(
    private val ratings: Map<String, AIMessageRating>,
    private val onRate: (messageId: String, rating: AIMessageRating?) -> Unit,
) : ChatAiComponentFactory {

    @Composable
    override fun AIConversationReplyActions(params: AIConversationReplyActionsParams) {
        AIReplyActionsRow(
            text = params.text,
            actions = AIReplyActions(
                rating = ratings[params.message.id],
                onRate = { rating -> onRate(params.message.id, rating) },
            ),
            modifier = params.modifier,
        )
    }
}
```

The screen puts them together:

```kotlin
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Phone
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import io.getstream.chat.android.ai.compose.conversation.AIMessageRating
import io.getstream.chat.android.ai.compose.conversation.AIMessageRole
import io.getstream.chat.android.ai.compose.ui.component.AIConversation
import io.getstream.chat.android.ai.compose.ui.component.AISuggestions
import io.getstream.chat.android.ai.compose.ui.component.ChatComposer
import io.getstream.chat.android.ai.compose.ui.component.LocalChatAiComponentFactory

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SupportScreen(viewModel: SupportViewModel = viewModel()) {
    val chat by viewModel.chat.collectAsStateWithLifecycle()
    var isCalling by remember { mutableStateOf(false) }

    if (isCalling) {
        VoiceAgentScreen(agent = "support", onClose = { isCalling = false })
        return
    }
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Help") },
                actions = {
                    IconButton(onClick = { isCalling = true }) {
                        Icon(Icons.Default.Phone, contentDescription = "Call support")
                    }
                },
            )
        },
    ) { padding ->
        val current = chat
        if (current == null) {
            Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
                CircularProgressIndicator()
            }
        } else {
            SupportConversation(current, Modifier.padding(padding))
        }
    }
}

@Composable
fun SupportConversation(chat: AgentChat, modifier: Modifier = Modifier) {
    val messages by chat.messages.collectAsStateWithLifecycle()
    val answering by chat.isAnswering.collectAsStateWithLifecycle()
    // How the user rated each reply, by message.
    val ratings = remember { mutableStateMapOf<String, AIMessageRating>() }
    val replyActions = remember {
        SupportReplyActions(ratings) { id, rating ->
            if (rating == null) ratings.remove(id) else ratings[id] = rating
        }
    }

    CompositionLocalProvider(LocalChatAiComponentFactory provides replyActions) {
        Column(modifier) {
            AIConversation(
                messages = messages,
                modifier = Modifier.weight(1f),
                isThinking = answering && messages.lastOrNull()?.role != AIMessageRole.Assistant,
                approver = chat.approver,
                emptyContent = {
                    AISuggestions(
                        suggestions = listOf("Where is my order?", "I want to return an item", "Talk to a person"),
                        onSuggestionClick = chat::send,
                    )
                },
            )
            ChatComposer(
                onSendClick = { chat.send(it.text) },
                onStopClick = chat::stop,
                isGenerating = answering,
            )
        }
    }
}
```

`VoiceAgentScreen` is the call screen of the [Voice agent](https://getstream.io/chat/docs/sdk/android/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. The quickstart's `chat.approver` answers 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=android) lets your team follow along.

## Send ratings to your backend

Ratings stay on the device unless you send them. In `SupportConversation`, send each one from `onRate`:

```kotlin
val scope = rememberCoroutineScope()
val replyActions = remember {
    SupportReplyActions(ratings) { id, rating ->
        if (rating == null) ratings.remove(id) else ratings[id] = rating
        scope.launch { yourBackend.rate(sessionId = chat.session.id, messageId = id, rating = rating) }
    }
}
```

`rating` is `AIMessageRating.Positive`, `AIMessageRating.Negative`, or `null` when the user takes their rating back. The router also reviews and scores each session once it ends, which [Sessions](https://getstream.io/agents/docs/sessions/?lang=android#title-description-and-review) describes.

## Next steps

- [Voice agent](https://getstream.io/chat/docs/sdk/android/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=android) and [Transfer to a person](https://getstream.io/agents/docs/telephony/transfer/?lang=android): the same agent on the phone.
- [Tool calling & Dispatch](https://getstream.io/agents/docs/tool-calling-and-dispatch/?lang=android): tools, approvals, timeouts and progress updates.

---

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