e071084ebe
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
96 lines
4.7 KiB
Markdown
96 lines
4.7 KiB
Markdown
---
|
|
layout: blog
|
|
title: "Building a Support Agent in Go"
|
|
permalink: /blog/28
|
|
description: "A real thing you can build with Go Micro: a support desk where a customer ticket triggers an agent that looks up the customer, sets priority, and replies — with a human-in-the-loop gate on the one action that touches a customer."
|
|
---
|
|
|
|
# Building a Support Agent in Go
|
|
|
|
*June 19, 2026 • Asim Aslam*
|
|
|
|
Most agent demos are a chat box wired to one tool. Real systems aren't that — they're a handful of services, an agent that operates them, something that triggers the agent without a human typing, and a gate on the actions you don't want it taking on its own. Here's that, built end to end. The full code is in [`examples/support`](https://github.com/micro/go-micro/tree/master/examples/support); it runs with no API key.
|
|
|
|
The scenario: a customer files a ticket. That should trigger an agent to look the customer up, set a priority, and reply — but it must not email anyone without passing an approval gate.
|
|
|
|
## 1. Services are the tools
|
|
|
|
Start with plain services. The agent will discover their endpoints as tools automatically — you don't describe them twice.
|
|
|
|
```go
|
|
type CustomerService struct{}
|
|
|
|
// Lookup returns the customer with the given email.
|
|
// @example {"email": "alice@acme.com"}
|
|
func (s *CustomerService) Lookup(ctx context.Context, req *LookupRequest, rsp *Customer) error {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
A `tickets` service (with `Update`) and a `notify` service (with `Send`, the action we'll gate) round it out. Three ordinary Go Micro services — nothing AI-specific about them.
|
|
|
|
## 2. The agent
|
|
|
|
An agent is a service with a model inside. Give it the services it manages and it turns their endpoints into tools:
|
|
|
|
```go
|
|
support := micro.NewAgent("support",
|
|
micro.AgentServices("customers", "tickets", "notify"),
|
|
micro.AgentPrompt("You are a support agent. For each ticket, look up the "+
|
|
"customer, set a priority, and reply. Escalate billing issues."),
|
|
)
|
|
```
|
|
|
|
That's the whole agent. It discovers `customers.Lookup`, `tickets.Update`, and `notify.Send`, and the model decides which to call.
|
|
|
|
## 3. The event is the prompt
|
|
|
|
A support agent that waits for someone to type into a chat box is useless. The trigger is a *ticket*, so a flow turns that event into the agent's work:
|
|
|
|
```go
|
|
intake := micro.NewFlow("intake",
|
|
micro.FlowTrigger("events.ticket.created"),
|
|
micro.FlowAgent("support"),
|
|
micro.FlowPrompt("A new support ticket arrived: {{.Data}}. Handle it."),
|
|
)
|
|
```
|
|
|
|
Now a `ticket.created` event on the broker is enough to set the agent going — no human in the loop to start it. ([When the event is the prompt](/blog/21) is the idea in full.)
|
|
|
|
## 4. The guardrail is the point
|
|
|
|
The agent can read and triage all it likes. The action you actually care about is the one that reaches a customer — sending the email. That goes through a gate:
|
|
|
|
```go
|
|
micro.AgentApproveTool(func(tool string, input map[string]any) (bool, string) {
|
|
if strings.Contains(tool, "Send") {
|
|
// return false to hold it for a human or a policy
|
|
log.Printf("approval gate: emailing %v", input["to"])
|
|
}
|
|
return true, ""
|
|
})
|
|
```
|
|
|
|
Return `false` and the send is refused with a reason the model sees — that's your human-in-the-loop, your spend cap, your billing sign-off. The agent never gets to email a customer on its own unless you let it. ([Agent guardrails](/blog/23) covers the rest.)
|
|
|
|
## Run it
|
|
|
|
```
|
|
> event: events.ticket.created {"id":"ticket-1","customer":"alice@acme.com",...}
|
|
|
|
[customers] looked up Alice (pro plan)
|
|
[tickets] ticket-1 → priority=high status=in_progress
|
|
▣ approval gate notify_NotifyService_Send(alice@acme.com) — approved
|
|
[notify] 📨 to=alice@acme.com: "Hi Alice — thanks for reaching out..."
|
|
|
|
✓ ticket triaged and the customer was replied to — triggered by an event
|
|
```
|
|
|
|
With the mock model it follows a fixed triage so it runs anywhere. Point it at a real model — `go run main.go -provider anthropic` — and the agent reasons about the ticket itself, choosing what to look up and how to reply.
|
|
|
|
## What you actually built
|
|
|
|
Count what's AI-specific: the prompt, and one model option. Everything else is services, a flow, the broker, and a guardrail — the things Go Micro has always done. The agent didn't need a framework of its own; it's a service that calls other services, triggered by an event, with a gate on the dangerous action.
|
|
|
|
That's the shape of a real agent system, and it's the same shape as a real service system. From here you'd make the gate enforce a real policy, add a knowledge-base service for the agent to search, or expose the agent over [A2A](/blog/26) so another team's agent can file tickets. The code is in [`examples/support`](https://github.com/micro/go-micro/tree/master/examples/support) — clone it and change one thing.
|