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.
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 textIts instructions say what it's for:
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:
- https://help.acme.com/shipping
- https://help.acme.com/returns---
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 --syncKnowledge & 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:
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
- Voice agent: the call screen the Call support button opens.
- Purchase a phone number and Transfer to a person: the same agent on the phone.
- Tool calling & Dispatch: tools, approvals, timeouts and progress updates.