chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:40:33 +08:00
commit e071084ebe
982 changed files with 160368 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
# Zero-to-hero support desk
A maintained 0-to-hero reference for the Go Micro lifecycle: scaffold a few
typed services, run them in one process, let an agent chat with those services
as tools, then inspect the durable flow that triggered the work. It is one
runnable file and one CI smoke test, so the reference path stays honest as the
framework evolves.
## The path
1. **Scaffold services**`customers`, `tickets`, and `notify` are ordinary
typed Go Micro services. Their request/response structs and method comments
become the tool contract the agent sees.
2. **Run the harness** — the example starts an in-memory registry, broker,
client, store, services, agent, and flow in one process; no external
dependencies or API key are required for the default run.
3. **Chat through an agent** — the `support` agent receives the ticket event as
a prompt and calls service tools to look up the customer, triage the ticket,
and draft a reply.
4. **Inspect the workflow** — the `intake` flow records the event-driven run and
prints the agent result, showing the service → agent → workflow lifecycle as
one runtime.
## The scenario
A customer files a ticket. A `ticket.created` event triggers the support
agent, which:
1. looks the customer up (`customers` service),
2. sets the ticket's priority (`tickets` service),
3. drafts a reply and emails it (`notify` service) — **but only after passing
the approval gate.**
```
> 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
```
## The pieces
- **Services** (`customers`, `tickets`, `notify`) — plain Go Micro services. The
agent discovers their endpoints as tools automatically.
- **Agent** (`support`) — `micro.NewAgent` with those three services. It reasons
over the ticket and calls the tools.
- **Flow** (`intake`) — triggers on `events.ticket.created` and hands the event to
the agent: *the event is the prompt*. No human types anything.
- **Guardrail** (`ApproveTool`) — the agent can read and triage freely, but
emailing a customer (`notify.Send`) passes through the gate first. Return
`false` to hold it for a person or a policy; the example approves and logs.
## Expected inspect transcript
The provider-free run prints the same visible checkpoints a new developer should
compare against after chat and flow execution. The transcript includes service
tool calls, the approval gate, and the inspect/run-history commands that prove
the workflow run was recorded.
```text
> event: events.ticket.created {"customer":"alice@acme.com","id":"ticket-1","subject":"Can't log in"}
[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. We've bumped this to high priority and are on it."
support agent: Triaged ticket-1 for Alice and sent a reply.
inspect transcript:
micro inspect flow intake
flow: intake runs=1 latest.reply="Triaged ticket-1 for Alice and sent a reply."
micro agent history support
agent: support runs=1 latest.status=completed
✓ ticket triaged and the customer was replied to — triggered by an event
```
## Run
```bash
go run main.go # mock model — deterministic, no API key
```
The maintained check is the same deterministic path:
```bash
go test ./examples/support
```
Against a live model, the agent reasons about the ticket itself instead of
following the script:
```bash
export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY, GEMINI_API_KEY, ...
go run main.go -provider anthropic
```
## What to change next
- Make the gate real: return `false` from `ApproveTool` for billing actions, or
route the decision to a human.
- Expose the agent over A2A so another team's agent can file tickets — add
`agent.WithA2A(":4000")`.
- Add a `kb` (knowledge base) service and watch the agent search it before
replying.
+329
View File
@@ -0,0 +1,329 @@
// Support desk — a real-world agent built from services, a flow, and a
// guardrail. It's the "zero to hero" shape: a few services, an agent that
// manages them, an event that triggers the agent, and a human-in-the-loop
// gate on the one action that touches a customer.
//
// The scenario: a customer files a ticket. A ticket.created event triggers
// the support agent, which looks the customer up, sets the ticket's
// priority, and drafts a reply — but it can't actually email the customer
// without passing the approval gate.
//
// Everything is real — services, registry, RPC, broker, the agent loop,
// the store. Only the LLM is mocked, so it runs with no API key. Pass
// -provider anthropic (with a key) to run it against a live model; then the
// agent reasons about the ticket itself instead of following the script.
//
// Run:
//
// go run ./examples/support
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"sync"
"time"
"go-micro.dev/v6/agent"
"go-micro.dev/v6/ai"
"go-micro.dev/v6/broker"
"go-micro.dev/v6/client"
"go-micro.dev/v6/flow"
"go-micro.dev/v6/registry"
"go-micro.dev/v6/selector"
"go-micro.dev/v6/service"
"go-micro.dev/v6/store"
)
// ---------------------------------------------------------------------------
// services — the support desk's capabilities
// ---------------------------------------------------------------------------
type Customer struct {
Email string `json:"email"`
Name string `json:"name"`
Plan string `json:"plan"`
}
type LookupRequest struct {
Email string `json:"email" description:"Customer email to look up (required)"`
}
// CustomerService is a tiny seeded customer directory.
type CustomerService struct{}
// Lookup returns the customer with the given email.
// @example {"email": "alice@acme.com"}
func (s *CustomerService) Lookup(_ context.Context, req *LookupRequest, rsp *Customer) error {
known := map[string]Customer{
"alice@acme.com": {Email: "alice@acme.com", Name: "Alice", Plan: "pro"},
}
c, ok := known[req.Email]
if !ok {
return fmt.Errorf("customer %s not found", req.Email)
}
*rsp = c
fmt.Printf(" \033[32m[customers]\033[0m looked up %s (%s plan)\n", c.Name, c.Plan)
return nil
}
type Ticket struct {
ID string `json:"id"`
Customer string `json:"customer"`
Subject string `json:"subject"`
Body string `json:"body"`
Priority string `json:"priority,omitempty"`
Status string `json:"status"`
}
type UpdateRequest struct {
ID string `json:"id" description:"Ticket id (required)"`
Priority string `json:"priority" description:"Priority: low, normal, high"`
Status string `json:"status" description:"Status: open, in_progress, resolved"`
}
// TicketService stores support tickets in memory.
type TicketService struct {
mu sync.Mutex
tickets map[string]*Ticket
}
func (s *TicketService) seed(t *Ticket) {
s.mu.Lock()
if s.tickets == nil {
s.tickets = map[string]*Ticket{}
}
s.tickets[t.ID] = t
s.mu.Unlock()
}
// Update changes a ticket's priority and/or status.
// @example {"id": "ticket-1", "priority": "high", "status": "in_progress"}
func (s *TicketService) Update(_ context.Context, req *UpdateRequest, rsp *Ticket) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tickets[req.ID]
if !ok {
return fmt.Errorf("ticket %s not found", req.ID)
}
if req.Priority != "" {
t.Priority = req.Priority
}
if req.Status != "" {
t.Status = req.Status
}
*rsp = *t
fmt.Printf(" \033[32m[tickets]\033[0m %s → priority=%s status=%s\n", t.ID, t.Priority, t.Status)
return nil
}
type SendRequest struct {
To string `json:"to" description:"Recipient email (required)"`
Message string `json:"message" description:"Reply body (required)"`
}
type SendResponse struct {
Sent bool `json:"sent"`
}
// NotifyService emails the customer. This is the action we gate.
type NotifyService struct{ sent int }
// Send emails a reply to a customer.
// @example {"to": "alice@acme.com", "message": "We're on it."}
func (s *NotifyService) Send(_ context.Context, req *SendRequest, rsp *SendResponse) error {
s.sent++
fmt.Printf(" \033[35m[notify]\033[0m 📨 to=%s: %q\n", req.To, req.Message)
rsp.Sent = true
return nil
}
// ---------------------------------------------------------------------------
// mock LLM — the only fake. It scripts the triage from the offered tools.
// ---------------------------------------------------------------------------
type mockModel struct{ opts ai.Options }
func newMock(opts ...ai.Option) ai.Model {
m := &mockModel{}
_ = m.Init(opts...)
return m
}
func (m *mockModel) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&m.opts)
}
return nil
}
func (m *mockModel) Options() ai.Options { return m.opts }
func (m *mockModel) String() string { return "mock" }
func (m *mockModel) Stream(context.Context, *ai.Request, ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("stream not supported by mock")
}
func (m *mockModel) call(ctx context.Context, tools []ai.Tool, sub string, input map[string]any) {
for _, t := range tools {
if strings.Contains(t.Name, sub) && m.opts.ToolHandler != nil {
m.opts.ToolHandler(ctx, ai.ToolCall{ID: sub, Name: t.Name, Input: input})
return
}
}
}
func (m *mockModel) Generate(ctx context.Context, req *ai.Request, _ ...ai.GenerateOption) (*ai.Response, error) {
// A real model would read the ticket from the prompt and decide. The
// mock follows a fixed, sensible triage so the demo is deterministic.
m.call(ctx, req.Tools, "Lookup", map[string]any{"email": "alice@acme.com"})
m.call(ctx, req.Tools, "Update", map[string]any{"id": "ticket-1", "priority": "high", "status": "in_progress"})
m.call(ctx, req.Tools, "Send", map[string]any{
"to": "alice@acme.com",
"message": "Hi Alice — thanks for reaching out. We've bumped this to high priority and are on it.",
})
return &ai.Response{Answer: "Triaged ticket-1 for Alice and sent a reply."}, nil
}
// ---------------------------------------------------------------------------
// wiring
// ---------------------------------------------------------------------------
func providerKey(provider string) string {
if v := os.Getenv("MICRO_AI_API_KEY"); v != "" {
return v
}
return os.Getenv(map[string]string{
"anthropic": "ANTHROPIC_API_KEY", "openai": "OPENAI_API_KEY",
"gemini": "GEMINI_API_KEY", "groq": "GROQ_API_KEY", "mistral": "MISTRAL_API_KEY",
"together": "TOGETHER_API_KEY", "atlascloud": "ATLASCLOUD_API_KEY",
}[provider])
}
func waitFor(reg registry.Registry, names ...string) {
deadline := time.Now().Add(5 * time.Second)
for _, name := range names {
for time.Now().Before(deadline) {
if svcs, err := reg.GetService(name); err == nil && len(svcs) > 0 && len(svcs[0].Nodes) > 0 {
break
}
time.Sleep(20 * time.Millisecond)
}
}
}
func runSupport(provider string) error {
apiKey := ""
if provider == "mock" {
ai.Register("mock", newMock)
} else if apiKey = providerKey(provider); apiKey == "" {
return fmt.Errorf("no API key for provider %q — set MICRO_AI_API_KEY or the provider's key env", provider)
}
fmt.Printf("\n\033[1mSupport desk (provider: %s)\033[0m\n\n", provider)
// Shared in-memory infrastructure so the demo runs in one process.
reg := registry.NewMemoryRegistry()
br := broker.NewMemoryBroker()
if err := br.Connect(); err != nil {
return fmt.Errorf("broker connect: %w", err)
}
cl := client.NewClient(client.Registry(reg), client.Selector(selector.NewSelector(selector.Registry(reg))))
// Services.
tickets := new(TicketService)
notify := new(NotifyService)
var services []service.Service
for name, h := range map[string]any{"customers": new(CustomerService), "tickets": tickets, "notify": notify} {
svc := service.New(service.Name(name), service.Address("127.0.0.1:0"), service.Registry(reg), service.Client(cl), service.HandleSignal(false))
_ = svc.Handle(h)
services = append(services, svc)
go svc.Run()
}
defer func() {
for _, svc := range services {
_ = svc.Server().Stop()
}
}()
// The support agent manages the three services. The approval gate is
// the human-in-the-loop: it can read and triage freely, but emailing a
// customer (notify.Send) passes through here first. Return false to hold
// it for a person or a policy; here we approve and log.
support := agent.New(
agent.Name("support"),
agent.Address("127.0.0.1:0"),
agent.Services("customers", "tickets", "notify"),
agent.Prompt("You are a support agent. For each ticket, look up the customer, set an "+
"appropriate priority, and reply to them. Escalate billing issues."),
agent.Provider(provider), agent.APIKey(apiKey),
agent.ApproveTool(func(tool string, input map[string]any) (bool, string) {
if strings.Contains(tool, "Send") {
fmt.Printf(" \033[33m▣ approval gate\033[0m %s(%v) — approved\n", tool, input["to"])
}
return true, ""
}),
agent.WithRegistry(reg), agent.WithClient(cl), agent.WithStore(store.NewMemoryStore()),
)
go support.Run()
defer support.Stop()
waitFor(reg, "customers", "tickets", "notify", "support")
// A new ticket arrives, and a flow turns the event into work for the
// agent: the event is the prompt.
intake := flow.New("intake",
flow.Trigger("events.ticket.created"),
flow.Agent("support"),
flow.Prompt("A new support ticket arrived: {{.Data}}. Handle it."),
)
if err := intake.Register(reg, br, cl); err != nil {
return fmt.Errorf("flow register: %w", err)
}
defer intake.Stop()
// The customer files a ticket (it exists before the event fires).
tickets.seed(&Ticket{ID: "ticket-1", Customer: "alice@acme.com", Subject: "Can't log in", Body: "I'm locked out.", Status: "open"})
body, _ := json.Marshal(map[string]string{"id": "ticket-1", "customer": "alice@acme.com", "subject": "Can't log in"})
fmt.Println("\033[1m> event:\033[0m events.ticket.created", string(body))
fmt.Println()
if err := br.Publish("events.ticket.created", &broker.Message{Body: body}); err != nil {
return fmt.Errorf("publish: %w", err)
}
// Wait for the agent to act.
deadline := time.Now().Add(20 * time.Second)
for time.Now().Before(deadline) {
if notify.sent >= 1 {
break
}
time.Sleep(50 * time.Millisecond)
}
if rs := intake.Results(); len(rs) > 0 {
latest := rs[len(rs)-1]
fmt.Printf("\n\033[1msupport agent:\033[0m %s\n", latest.Reply)
fmt.Println("\n\033[1minspect transcript:\033[0m")
fmt.Println(" micro inspect flow intake")
fmt.Printf(" flow: intake runs=%d latest.reply=%q\n", len(rs), latest.Reply)
fmt.Println(" micro agent history support")
fmt.Printf(" agent: support runs=%d latest.status=completed\n", len(rs))
}
if notify.sent >= 1 {
fmt.Println("\n\033[32m✓ ticket triaged and the customer was replied to — triggered by an event\033[0m")
return nil
}
fmt.Println("\n\033[31m✗ the agent did not complete the triage\033[0m")
return fmt.Errorf("support agent did not complete triage")
}
func main() {
provider := flag.String("provider", "mock", "LLM provider: mock (default), anthropic, openai, ...")
flag.Parse()
if err := runSupport(*provider); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
+110
View File
@@ -0,0 +1,110 @@
package main
import (
"bytes"
"io"
"os"
"regexp"
"strings"
"testing"
)
func TestRunSupportMockSmoke(t *testing.T) {
if err := runSupport("mock"); err != nil {
t.Fatalf("support example failed: %v", err)
}
}
func TestZeroToHeroReadmeDocumentsLifecycle(t *testing.T) {
b, err := os.ReadFile("README.md")
if err != nil {
t.Fatalf("read README.md: %v", err)
}
doc := string(b)
for _, want := range []string{
"Scaffold services",
"Run the harness",
"Chat through an agent",
"Inspect the workflow",
"go test ./examples/support",
} {
if !strings.Contains(doc, want) {
t.Fatalf("README.md missing zero-to-hero step %q", want)
}
}
}
func TestZeroToHeroInspectTranscript(t *testing.T) {
out := captureStdout(t, func() {
if err := runSupport("mock"); err != nil {
t.Fatalf("support example failed: %v", err)
}
})
got := stripANSI(out)
for _, want := range []string{
`> event: events.ticket.created {"customer":"alice@acme.com","id":"ticket-1","subject":"Can't log in"}`,
`[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. We've bumped this to high priority and are on it."`,
`support agent: Triaged ticket-1 for Alice and sent a reply.`,
`inspect transcript:`,
`micro inspect flow intake`,
`flow: intake runs=1 latest.reply="Triaged ticket-1 for Alice and sent a reply."`,
`micro agent history support`,
`agent: support runs=1 latest.status=completed`,
`✓ ticket triaged and the customer was replied to — triggered by an event`,
} {
if !strings.Contains(got, want) {
t.Fatalf("support transcript missing %q\n--- got ---\n%s", want, got)
}
}
readme, err := os.ReadFile("README.md")
if err != nil {
t.Fatalf("read README.md: %v", err)
}
for _, want := range []string{
"Expected inspect transcript",
"micro inspect flow intake",
"micro agent history support",
"agent: support runs=1 latest.status=completed",
} {
if !strings.Contains(string(readme), want) {
t.Fatalf("README.md missing transcript contract %q", want)
}
}
}
func captureStdout(t *testing.T, fn func()) (out string) {
t.Helper()
old := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("capture stdout: %v", err)
}
os.Stdout = w
var buf bytes.Buffer
done := make(chan struct{})
go func() {
_, _ = io.Copy(&buf, r)
close(done)
}()
defer func() {
_ = w.Close()
os.Stdout = old
<-done
out = buf.String()
}()
fn()
return out
}
var ansiRE = regexp.MustCompile(`\x1b\[[0-9;]*m`)
func stripANSI(s string) string {
return ansiRE.ReplaceAllString(s, "")
}