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, 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:

getstream agents init support --template text

Its instructions say what it's for:

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:

agents/support/knowledge/urls.yaml
- https://help.acme.com/shipping
- https://help.acme.com/returns
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:

getstream agents test support --sync

Knowledge & Skills and Guardrails cover both in full.

Host its tools on your backend

The tools that reach your systems run on your backend, in a dispatch worker. 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:

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 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:

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:

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 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 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:

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 describes.

Next steps